summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/accounts/AccountManagerService.java2
-rw-r--r--core/java/android/animation/Animatable.java146
-rwxr-xr-xcore/java/android/animation/Animator.java773
-rw-r--r--core/java/android/animation/DoubleEvaluator.java42
-rw-r--r--core/java/android/animation/FloatEvaluator.java42
-rw-r--r--core/java/android/animation/IntEvaluator.java42
-rw-r--r--core/java/android/animation/PropertyAnimator.java395
-rw-r--r--core/java/android/animation/RGBEvaluator.java59
-rw-r--r--core/java/android/animation/Sequencer.java681
-rw-r--r--core/java/android/animation/TypeEvaluator.java44
-rw-r--r--core/java/android/animation/package.html5
-rw-r--r--core/java/android/app/ActionBar.java531
-rw-r--r--core/java/android/app/Activity.java428
-rw-r--r--core/java/android/app/ActivityManagerNative.java37
-rw-r--r--core/java/android/app/ActivityThread.java103
-rw-r--r--core/java/android/app/ApplicationThreadNative.java28
-rw-r--r--core/java/android/app/BackStackEntry.java466
-rw-r--r--core/java/android/app/ContextImpl.java26
-rw-r--r--core/java/android/app/Fragment.java770
-rw-r--r--core/java/android/app/FragmentManager.java1000
-rw-r--r--core/java/android/app/FragmentTransaction.java151
-rw-r--r--core/java/android/app/IActivityManager.java7
-rw-r--r--core/java/android/app/IApplicationThread.java3
-rw-r--r--core/java/android/app/Instrumentation.java96
-rw-r--r--core/java/android/app/ListActivity.java2
-rw-r--r--core/java/android/app/ListFragment.java406
-rw-r--r--core/java/android/app/LoaderManager.java306
-rw-r--r--core/java/android/app/LoaderManagingFragment.java204
-rw-r--r--core/java/android/app/LocalActivityManager.java30
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java521
-rw-r--r--core/java/android/app/admin/IDevicePolicyManager.aidl26
-rw-r--r--core/java/android/appwidget/AppWidgetHostView.java5
-rw-r--r--core/java/android/appwidget/AppWidgetManager.java10
-rw-r--r--core/java/android/appwidget/AppWidgetProviderInfo.java13
-rw-r--r--core/java/android/bluetooth/BluetoothClass.java4
-rw-r--r--core/java/android/bluetooth/BluetoothInputDevice.java241
-rw-r--r--core/java/android/bluetooth/BluetoothUuid.java6
-rw-r--r--core/java/android/bluetooth/IBluetooth.aidl9
-rw-r--r--core/java/android/content/AsyncTaskLoader.java107
-rw-r--r--core/java/android/content/ContentResolver.java9
-rw-r--r--core/java/android/content/Context.java28
-rw-r--r--core/java/android/content/ContextWrapper.java7
-rw-r--r--core/java/android/content/CursorLoader.java158
-rw-r--r--core/java/android/content/Loader.java154
-rw-r--r--core/java/android/content/SharedPreferences.java26
-rw-r--r--core/java/android/content/SyncManager.java53
-rw-r--r--core/java/android/content/XmlDocumentProvider.java436
-rw-r--r--core/java/android/content/pm/ApplicationInfo.java6
-rw-r--r--core/java/android/content/pm/PackageParser.java6
-rw-r--r--core/java/android/content/res/PluralRules.java111
-rw-r--r--core/java/android/content/res/Resources.java58
-rw-r--r--core/java/android/database/AbstractCursor.java200
-rw-r--r--core/java/android/database/AbstractWindowedCursor.java181
-rw-r--r--core/java/android/database/BulkCursorNative.java69
-rw-r--r--core/java/android/database/BulkCursorToCursorAdaptor.java74
-rw-r--r--core/java/android/database/Cursor.java238
-rw-r--r--core/java/android/database/CursorToBulkCursorAdaptor.java31
-rw-r--r--core/java/android/database/CursorWindow.java76
-rw-r--r--core/java/android/database/CursorWrapper.java130
-rw-r--r--core/java/android/database/DataSetObservable.java12
-rw-r--r--core/java/android/database/DatabaseErrorHandler.java (renamed from core/java/android/pim/vcard/exception/VCardException.java)30
-rw-r--r--core/java/android/database/DatabaseUtils.java31
-rw-r--r--core/java/android/database/DefaultDatabaseErrorHandler.java94
-rw-r--r--core/java/android/database/IBulkCursor.java14
-rw-r--r--core/java/android/database/MatrixCursor.java5
-rw-r--r--core/java/android/database/MergeCursor.java31
-rw-r--r--core/java/android/database/RequeryOnUiThreadException.java (renamed from core/java/android/pim/vcard/exception/VCardVersionException.java)18
-rw-r--r--core/java/android/database/sqlite/DatabaseConnectionPool.java361
-rw-r--r--core/java/android/database/sqlite/DatabaseObjectNotClosedException.java6
-rw-r--r--core/java/android/database/sqlite/SQLiteCompiledSql.java29
-rw-r--r--core/java/android/database/sqlite/SQLiteCursor.java186
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java1075
-rw-r--r--core/java/android/database/sqlite/SQLiteDebug.java9
-rw-r--r--core/java/android/database/sqlite/SQLiteDirectCursorDriver.java6
-rw-r--r--core/java/android/database/sqlite/SQLiteOpenHelper.java37
-rw-r--r--core/java/android/database/sqlite/SQLiteProgram.java269
-rw-r--r--core/java/android/database/sqlite/SQLiteQuery.java5
-rw-r--r--core/java/android/database/sqlite/SQLiteStatement.java152
-rw-r--r--core/java/android/hardware/SensorEvent.java4
-rw-r--r--core/java/android/net/ConnectivityManager.java17
-rw-r--r--core/java/android/net/Downloads.java7
-rw-r--r--core/java/android/net/IConnectivityManager.aidl2
-rw-r--r--core/java/android/net/MobileDataStateTracker.java301
-rw-r--r--core/java/android/net/NetworkInfo.java8
-rw-r--r--core/java/android/net/NetworkProperties.aidl22
-rw-r--r--core/java/android/net/NetworkProperties.java196
-rw-r--r--core/java/android/net/NetworkStateTracker.java328
-rw-r--r--core/java/android/net/NetworkUtils.java73
-rw-r--r--core/java/android/net/ProxyProperties.java108
-rw-r--r--core/java/android/net/http/AndroidHttpClient.java47
-rw-r--r--core/java/android/net/http/CertificateChainValidator.java12
-rw-r--r--core/java/android/os/AsyncTask.java36
-rw-r--r--core/java/android/os/Debug.java26
-rw-r--r--core/java/android/os/storage/StorageEventListener.java1
-rw-r--r--core/java/android/os/storage/StorageManager.java2
-rw-r--r--core/java/android/os/storage/StorageResultCode.java2
-rw-r--r--core/java/android/pim/RecurrenceSet.java9
-rw-r--r--core/java/android/pim/vcard/JapaneseUtils.java380
-rw-r--r--core/java/android/pim/vcard/VCardBuilder.java1932
-rw-r--r--core/java/android/pim/vcard/VCardComposer.java592
-rw-r--r--core/java/android/pim/vcard/VCardConfig.java477
-rw-r--r--core/java/android/pim/vcard/VCardConstants.java152
-rw-r--r--core/java/android/pim/vcard/VCardEntry.java1447
-rw-r--r--core/java/android/pim/vcard/VCardEntryCommitter.java68
-rw-r--r--core/java/android/pim/vcard/VCardEntryConstructor.java305
-rw-r--r--core/java/android/pim/vcard/VCardEntryCounter.java63
-rw-r--r--core/java/android/pim/vcard/VCardEntryHandler.java38
-rw-r--r--core/java/android/pim/vcard/VCardInterpreter.java102
-rw-r--r--core/java/android/pim/vcard/VCardInterpreterCollection.java102
-rw-r--r--core/java/android/pim/vcard/VCardParser.java101
-rw-r--r--core/java/android/pim/vcard/VCardParser_V21.java936
-rw-r--r--core/java/android/pim/vcard/VCardParser_V30.java358
-rw-r--r--core/java/android/pim/vcard/VCardSourceDetector.java129
-rw-r--r--core/java/android/pim/vcard/VCardUtils.java545
-rw-r--r--core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java27
-rw-r--r--core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java32
-rw-r--r--core/java/android/pim/vcard/exception/VCardInvalidLineException.java32
-rw-r--r--core/java/android/pim/vcard/exception/VCardNestedException.java29
-rw-r--r--core/java/android/pim/vcard/exception/VCardNotSupportedException.java33
-rw-r--r--core/java/android/pim/vcard/exception/package.html5
-rw-r--r--core/java/android/pim/vcard/package.html5
-rw-r--r--core/java/android/preference/MultiSelectListPreference.java274
-rw-r--r--core/java/android/preference/Preference.java63
-rw-r--r--core/java/android/preference/PreferenceActivity.java124
-rw-r--r--core/java/android/provider/Calendar.java325
-rw-r--r--core/java/android/provider/ContactsContract.java290
-rw-r--r--core/java/android/provider/MediaStore.java99
-rw-r--r--core/java/android/provider/Mtp.java335
-rw-r--r--core/java/android/provider/Settings.java44
-rw-r--r--core/java/android/server/BluetoothEventLoop.java52
-rw-r--r--core/java/android/server/BluetoothService.java131
-rw-r--r--core/java/android/text/AndroidBidi.java129
-rw-r--r--core/java/android/text/BoringLayout.java25
-rw-r--r--core/java/android/text/DynamicLayout.java1
-rw-r--r--core/java/android/text/GraphicsOperations.java22
-rw-r--r--core/java/android/text/Layout.java1291
-rw-r--r--core/java/android/text/MeasuredText.java229
-rw-r--r--core/java/android/text/Selection.java4
-rw-r--r--core/java/android/text/SpannableStringBuilder.java127
-rw-r--r--core/java/android/text/Spanned.java2
-rw-r--r--core/java/android/text/StaticLayout.java690
-rw-r--r--core/java/android/text/Styled.java434
-rw-r--r--core/java/android/text/TextLine.java940
-rw-r--r--core/java/android/text/TextUtils.java442
-rw-r--r--core/java/android/text/method/ArrowKeyMovementMethod.java350
-rw-r--r--core/java/android/text/method/Touch.java13
-rw-r--r--core/java/android/util/CharsetUtils.java70
-rw-r--r--core/java/android/util/Patterns.java12
-rw-r--r--core/java/android/util/SparseArray.java10
-rw-r--r--core/java/android/view/AbsSavedState.java4
-rw-r--r--core/java/android/view/GLES20Canvas.java610
-rw-r--r--core/java/android/view/HardwareRenderer.java562
-rw-r--r--core/java/android/view/LayoutInflater.java50
-rw-r--r--core/java/android/view/MenuInflater.java67
-rw-r--r--core/java/android/view/MenuItem.java24
-rw-r--r--core/java/android/view/MotionEvent.java2
-rw-r--r--core/java/android/view/SurfaceView.java3
-rw-r--r--core/java/android/view/View.java66
-rw-r--r--core/java/android/view/ViewGroup.java74
-rw-r--r--core/java/android/view/ViewRoot.java283
-rw-r--r--core/java/android/view/Window.java19
-rw-r--r--core/java/android/view/accessibility/AccessibilityEvent.java1
-rw-r--r--core/java/android/view/accessibility/AccessibilityManager.java27
-rw-r--r--core/java/android/view/accessibility/IAccessibilityManager.aidl2
-rw-r--r--core/java/android/view/animation/Animation.java40
-rw-r--r--core/java/android/view/animation/AnimationSet.java2
-rw-r--r--core/java/android/view/animation/RotateAnimation.java5
-rw-r--r--core/java/android/view/animation/ScaleAnimation.java3
-rw-r--r--core/java/android/webkit/AccessibilityInjector.java99
-rw-r--r--core/java/android/webkit/BrowserFrame.java58
-rw-r--r--core/java/android/webkit/CallbackProxy.java104
-rwxr-xr-xcore/java/android/webkit/GeolocationService.java25
-rw-r--r--core/java/android/webkit/HTML5Audio.java224
-rw-r--r--core/java/android/webkit/JWebCoreJavaBridge.java27
-rw-r--r--core/java/android/webkit/MimeTypeMap.java1
-rw-r--r--core/java/android/webkit/Network.java52
-rw-r--r--core/java/android/webkit/WebChromeClient.java28
-rw-r--r--core/java/android/webkit/WebHistoryItem.java40
-rw-r--r--core/java/android/webkit/WebSettings.java167
-rw-r--r--core/java/android/webkit/WebTextView.java59
-rw-r--r--core/java/android/webkit/WebView.java2018
-rw-r--r--core/java/android/webkit/WebViewCore.java394
-rw-r--r--core/java/android/webkit/WebViewDatabase.java263
-rw-r--r--core/java/android/webkit/ZoomControlBase.java41
-rw-r--r--core/java/android/webkit/ZoomControlEmbedded.java117
-rw-r--r--core/java/android/webkit/ZoomControlExternal.java159
-rw-r--r--core/java/android/webkit/ZoomManager.java902
-rw-r--r--core/java/android/webruntime/WebRuntimeActivity.java204
-rw-r--r--core/java/android/widget/AbsListView.java103
-rw-r--r--core/java/android/widget/Adapters.java1232
-rw-r--r--core/java/android/widget/AutoCompleteTextView.java680
-rw-r--r--core/java/android/widget/CursorAdapter.java50
-rw-r--r--core/java/android/widget/Gallery.java3
-rw-r--r--core/java/android/widget/GridView.java16
-rw-r--r--core/java/android/widget/LinearLayout.java43
-rw-r--r--core/java/android/widget/ListPopupWindow.java1228
-rw-r--r--core/java/android/widget/ListView.java32
-rw-r--r--core/java/android/widget/PopupWindow.java35
-rw-r--r--core/java/android/widget/ProgressBar.java3
-rw-r--r--core/java/android/widget/QuickContactBadge.java11
-rw-r--r--core/java/android/widget/RemoteViews.java5
-rw-r--r--core/java/android/widget/SimpleCursorAdapter.java3
-rw-r--r--core/java/android/widget/Spinner.java201
-rw-r--r--core/java/android/widget/TextView.java893
-rw-r--r--core/java/android/widget/ViewAnimator.java41
-rw-r--r--core/java/android/widget/ViewFlipper.java18
-rw-r--r--core/java/android/widget/ZoomButtonsController.java5
-rw-r--r--core/java/com/android/internal/app/ActionBarImpl.java469
-rw-r--r--core/java/com/android/internal/database/SortCursor.java23
-rw-r--r--core/java/com/android/internal/os/SamplingProfilerIntegration.java140
-rw-r--r--core/java/com/android/internal/os/ZygoteInit.java19
-rw-r--r--core/java/com/android/internal/util/HierarchicalStateMachine.java29
-rw-r--r--core/java/com/android/internal/util/XmlUtils.java96
-rw-r--r--core/java/com/android/internal/view/menu/ActionMenu.java263
-rw-r--r--core/java/com/android/internal/view/menu/ActionMenuItem.java225
-rw-r--r--core/java/com/android/internal/view/menu/ActionMenuItemView.java112
-rw-r--r--core/java/com/android/internal/view/menu/ActionMenuView.java120
-rw-r--r--core/java/com/android/internal/view/menu/IconMenuView.java2
-rw-r--r--core/java/com/android/internal/view/menu/MenuBuilder.java122
-rw-r--r--core/java/com/android/internal/view/menu/MenuItemImpl.java37
-rw-r--r--core/java/com/android/internal/view/menu/MenuPopupHelper.java91
-rw-r--r--core/java/com/android/internal/widget/ActionBarContextView.java309
-rw-r--r--core/java/com/android/internal/widget/ActionBarView.java658
-rw-r--r--core/java/com/android/internal/widget/ContactHeaderWidget.java661
-rw-r--r--core/java/com/android/internal/widget/DigitalClock.java4
-rw-r--r--core/java/com/android/internal/widget/EditStyledText.java1663
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java145
-rw-r--r--core/java/com/android/internal/widget/SlidingTab.java44
228 files changed, 26544 insertions, 17923 deletions
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java
index 1d9e0f1..2ead976 100644
--- a/core/java/android/accounts/AccountManagerService.java
+++ b/core/java/android/accounts/AccountManagerService.java
@@ -1657,7 +1657,7 @@ public class AccountManagerService
}
boolean needsProvisioning;
try {
- needsProvisioning = telephony.getCdmaNeedsProvisioning();
+ needsProvisioning = telephony.needsOtaServiceProvisioning();
} catch (RemoteException e) {
Log.w(TAG, "exception while checking provisioning", e);
// default to NOT wiping out the passwords
diff --git a/core/java/android/animation/Animatable.java b/core/java/android/animation/Animatable.java
new file mode 100644
index 0000000..68415f0
--- /dev/null
+++ b/core/java/android/animation/Animatable.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import java.util.ArrayList;
+
+/**
+ * This is the superclass for classes which provide basic support for animations which can be
+ * started, ended, and have <code>AnimatableListeners</code> added to them.
+ */
+public abstract class Animatable {
+
+
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ ArrayList<AnimatableListener> mListeners = null;
+
+ /**
+ * Starts this animation. If the animation has a nonzero startDelay, the animation will start
+ * running after that delay elapses. Note that the animation does not start synchronously with
+ * this call, because all animation events are posted to a central timing loop so that animation
+ * times are all synchronized on a single timing pulse on the UI thread. So the animation will
+ * start the next time that event handler processes events.
+ */
+ public void start() {
+ }
+
+ /**
+ * Cancels the animation. Unlike {@link #end()}, <code>cancel()</code> causes the animation to
+ * stop in its tracks, sending an {@link AnimatableListener#onAnimationCancel(Animatable)} to
+ * its listeners, followed by an {@link AnimatableListener#onAnimationEnd(Animatable)} message.
+ */
+ public void cancel() {
+ }
+
+ /**
+ * Ends the animation. This causes the animation to assign the end value of the property being
+ * animated, then calling the {@link AnimatableListener#onAnimationEnd(Animatable)} method on
+ * its listeners.
+ */
+ public void end() {
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent events through the life of an
+ * animation, such as start, repeat, and end.
+ *
+ * @param listener the listener to be added to the current set of listeners for this animation.
+ */
+ public void addListener(AnimatableListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<AnimatableListener>();
+ }
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of listeners for this
+ * animation.
+ */
+ public void removeListener(AnimatableListener listener) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ mListeners = null;
+ }
+ }
+
+ /**
+ * Gets the set of {@link AnimatableListener} objects that are currently
+ * listening for events on this <code>Animatable</code> object.
+ *
+ * @return ArrayList<AnimatableListener> The set of listeners.
+ */
+ public ArrayList<AnimatableListener> getListeners() {
+ return mListeners;
+ }
+
+ /**
+ * Removes all listeners from this object. This is equivalent to calling
+ * <code>getListeners()</code> followed by calling <code>clear()</code> on the
+ * returned list of listeners.
+ */
+ public void removeAllListeners() {
+ if (mListeners != null) {
+ mListeners.clear();
+ mListeners = null;
+ }
+ }
+
+ /**
+ * <p>An animation listener receives notifications from an animation.
+ * Notifications indicate animation related events, such as the end or the
+ * repetition of the animation.</p>
+ */
+ public static interface AnimatableListener {
+ /**
+ * <p>Notifies the start of the animation.</p>
+ *
+ * @param animation The started animation.
+ */
+ void onAnimationStart(Animatable animation);
+
+ /**
+ * <p>Notifies the end of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * @param animation The animation which reached its end.
+ */
+ void onAnimationEnd(Animatable animation);
+
+ /**
+ * <p>Notifies the cancellation of the animation. This callback is not invoked
+ * for animations with repeat count set to INFINITE.</p>
+ *
+ * @param animation The animation which was canceled.
+ */
+ void onAnimationCancel(Animatable animation);
+
+ /**
+ * <p>Notifies the repetition of the animation.</p>
+ *
+ * @param animation The animation which was repeated.
+ */
+ void onAnimationRepeat(Animatable animation);
+ }
+}
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
new file mode 100755
index 0000000..b6c4763
--- /dev/null
+++ b/core/java/android/animation/Animator.java
@@ -0,0 +1,773 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.os.Handler;
+import android.os.Message;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import java.util.ArrayList;
+
+/**
+ * This class provides a simple timing engine for running animations
+ * which calculate animated values and set them on target objects.
+ *
+ * There is a single timing pulse that all animations use. It runs in a
+ * custom handler to ensure that property changes happen on the UI thread.
+ */
+public class Animator extends Animatable {
+
+ /**
+ * Internal constants
+ */
+
+ /*
+ * The default amount of time in ms between animation frames
+ */
+ private static final long DEFAULT_FRAME_DELAY = 30;
+
+ /**
+ * Messages sent to timing handler: START is sent when an animation first begins, FRAME is sent
+ * by the handler to itself to process the next animation frame
+ */
+ private static final int ANIMATION_START = 0;
+ private static final int ANIMATION_FRAME = 1;
+
+ /**
+ * Values used with internal variable mPlayingState to indicate the current state of an
+ * animation.
+ */
+ private static final int STOPPED = 0; // Not yet playing
+ private static final int RUNNING = 1; // Playing normally
+ private static final int CANCELED = 2; // cancel() called - need to end it
+ private static final int ENDED = 3; // end() called - need to end it
+
+ /**
+ * Internal variables
+ */
+
+
+ // The first time that the animation's animateFrame() method is called. This time is used to
+ // determine elapsed time (and therefore the elapsed fraction) in subsequent calls
+ // to animateFrame()
+ private long mStartTime;
+
+ // The static sAnimationHandler processes the internal timing loop on which all animations
+ // are based
+ private static AnimationHandler sAnimationHandler;
+
+ // The static list of all active animations
+ private static final ArrayList<Animator> sAnimations = new ArrayList<Animator>();
+
+ // The set of animations to be started on the next animation frame
+ private static final ArrayList<Animator> sPendingAnimations = new ArrayList<Animator>();
+
+ // The time interpolator to be used if none is set on the animation
+ private static final Interpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator();
+
+ // type evaluators for the three primitive types handled by this implementation
+ private static final TypeEvaluator sIntEvaluator = new IntEvaluator();
+ private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator();
+ private static final TypeEvaluator sDoubleEvaluator = new DoubleEvaluator();
+
+ /**
+ * Used to indicate whether the animation is currently playing in reverse. This causes the
+ * elapsed fraction to be inverted to calculate the appropriate values.
+ */
+ private boolean mPlayingBackwards = false;
+
+ /**
+ * This variable tracks the current iteration that is playing. When mCurrentIteration exceeds the
+ * repeatCount (if repeatCount!=INFINITE), the animation ends
+ */
+ private int mCurrentIteration = 0;
+
+ /**
+ * Tracks whether a startDelay'd animation has begun playing through the startDelay.
+ */
+ private boolean mStartedDelay = false;
+
+ /**
+ * Tracks the time at which the animation began playing through its startDelay. This is
+ * different from the mStartTime variable, which is used to track when the animation became
+ * active (which is when the startDelay expired and the animation was added to the active
+ * animations list).
+ */
+ private long mDelayStartTime;
+
+ /**
+ * Flag that represents the current state of the animation. Used to figure out when to start
+ * an animation (if state == STOPPED). Also used to end an animation that
+ * has been cancel()'d or end()'d since the last animation frame. Possible values are
+ * STOPPED, RUNNING, ENDED, CANCELED.
+ */
+ private int mPlayingState = STOPPED;
+
+ /**
+ * Internal collections used to avoid set collisions as animations start and end while being
+ * processed.
+ */
+ private static final ArrayList<Animator> sEndingAnims = new ArrayList<Animator>();
+ private static final ArrayList<Animator> sDelayedAnims = new ArrayList<Animator>();
+ private static final ArrayList<Animator> sReadyAnims = new ArrayList<Animator>();
+
+ //
+ // Backing variables
+ //
+
+ // How long the animation should last in ms
+ private long mDuration;
+
+ // The value that the animation should start from, set in the constructor
+ private Object mValueFrom;
+
+ // The value that the animation should animate to, set in the constructor
+ private Object mValueTo;
+
+ // The amount of time in ms to delay starting the animation after start() is called
+ private long mStartDelay = 0;
+
+ // The number of milliseconds between animation frames
+ private static long sFrameDelay = DEFAULT_FRAME_DELAY;
+
+ // The number of times the animation will repeat. The default is 0, which means the animation
+ // will play only once
+ private int mRepeatCount = 0;
+
+ /**
+ * The type of repetition that will occur when repeatMode is nonzero. RESTART means the
+ * animation will start from the beginning on every new cycle. REVERSE means the animation
+ * will reverse directions on each iteration.
+ */
+ private int mRepeatMode = RESTART;
+
+ /**
+ * The time interpolator to be used. The elapsed fraction of the animation will be passed
+ * through this interpolator to calculate the interpolated fraction, which is then used to
+ * calculate the animated values.
+ */
+ private Interpolator mInterpolator = sDefaultInterpolator;
+
+ /**
+ * The type evaluator used to calculate the animated values. This evaluator is determined
+ * automatically based on the type of the start/end objects passed into the constructor,
+ * but the system only knows about the primitive types int, double, and float. Any other
+ * type will need to set the evaluator to a custom evaluator for that type.
+ */
+ private TypeEvaluator mEvaluator;
+
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ private ArrayList<AnimatorUpdateListener> mUpdateListeners = null;
+
+ /**
+ * The current value calculated by the animation. The value is calculated in animateFraction(),
+ * prior to calling the setter (if set) and sending out the onAnimationUpdate() callback
+ * to the update listeners.
+ */
+ private Object mAnimatedValue = null;
+
+ /**
+ * The type of the values, as determined by the valueFrom/valueTo properties.
+ */
+ Class mValueType;
+
+ /**
+ * Public constants
+ */
+
+ /**
+ * When the animation reaches the end and <code>repeatCount</code> is INFINITE
+ * or a positive value, the animation restarts from the beginning.
+ */
+ public static final int RESTART = 1;
+ /**
+ * When the animation reaches the end and <code>repeatCount</code> is INFINITE
+ * or a positive value, the animation reverses direction on every iteration.
+ */
+ public static final int REVERSE = 2;
+ /**
+ * This value used used with the {@link #setRepeatCount(int)} property to repeat
+ * the animation indefinitely.
+ */
+ public static final int INFINITE = -1;
+
+ private Animator(long duration, Object valueFrom, Object valueTo, Class valueType) {
+ mDuration = duration;
+ mValueFrom = valueFrom;
+ mValueTo= valueTo;
+ this.mValueType = valueType;
+ }
+
+ /**
+ * This function is called immediately before processing the first animation
+ * frame of an animation. If there is a nonzero <code>startDelay</code>, the
+ * function is called after that delay ends.
+ * It takes care of the final initialization steps for the
+ * animation.
+ *
+ * <p>Overrides of this method should call the superclass method to ensure
+ * that internal mechanisms for the animation are set up correctly.</p>
+ */
+ void initAnimation() {
+ if (mEvaluator == null) {
+ mEvaluator = (mValueType == int.class) ? sIntEvaluator :
+ (mValueType == double.class) ? sDoubleEvaluator : sFloatEvaluator;
+ }
+ mPlayingBackwards = false;
+ mCurrentIteration = 0;
+ }
+
+ /**
+ * A constructor that takes <code>float</code> values.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public Animator(long duration, float valueFrom, float valueTo) {
+ this(duration, valueFrom, valueTo, float.class);
+ }
+
+ /**
+ * A constructor that takes <code>int</code> values.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public Animator(long duration, int valueFrom, int valueTo) {
+ this(duration, valueFrom, valueTo, int.class);
+ }
+
+ /**
+ * A constructor that takes <code>double</code> values.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public Animator(long duration, double valueFrom, double valueTo) {
+ this(duration, valueFrom, valueTo, double.class);
+ }
+
+ /**
+ * A constructor that takes <code>Object</code> values.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public Animator(long duration, Object valueFrom, Object valueTo) {
+ this(duration, valueFrom, valueTo,
+ (valueFrom != null) ? valueFrom.getClass() : valueTo.getClass());
+ }
+
+ /**
+ * This custom, static handler handles the timing pulse that is shared by
+ * all active animations. This approach ensures that the setting of animation
+ * values will happen on the UI thread and that all animations will share
+ * the same times for calculating their values, which makes synchronizing
+ * animations possible.
+ *
+ */
+ private static class AnimationHandler extends Handler {
+ /**
+ * There are only two messages that we care about: ANIMATION_START and
+ * ANIMATION_FRAME. The START message is sent when an animation's start()
+ * method is called. It cannot start synchronously when start() is called
+ * because the call may be on the wrong thread, and it would also not be
+ * synchronized with other animations because it would not start on a common
+ * timing pulse. So each animation sends a START message to the handler, which
+ * causes the handler to place the animation on the active animations queue and
+ * start processing frames for that animation.
+ * The FRAME message is the one that is sent over and over while there are any
+ * active animations to process.
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ boolean callAgain = true;
+ switch (msg.what) {
+ // TODO: should we avoid sending frame message when starting if we
+ // were already running?
+ case ANIMATION_START:
+ if (sAnimations.size() > 0 || sDelayedAnims.size() > 0) {
+ callAgain = false;
+ }
+ // pendingAnims holds any animations that have requested to be started
+ // We're going to clear sPendingAnimations, but starting animation may
+ // cause more to be added to the pending list (for example, if one animation
+ // starting triggers another starting). So we loop until sPendingAnimations
+ // is empty.
+ while (sPendingAnimations.size() > 0) {
+ ArrayList<Animator> pendingCopy =
+ (ArrayList<Animator>) sPendingAnimations.clone();
+ sPendingAnimations.clear();
+ int count = pendingCopy.size();
+ for (int i = 0; i < count; ++i) {
+ Animator anim = pendingCopy.get(i);
+ // If the animation has a startDelay, place it on the delayed list
+ if (anim.mStartDelay == 0) {
+ anim.startAnimation();
+ } else {
+ sDelayedAnims.add(anim);
+ }
+ }
+ }
+ // fall through to process first frame of new animations
+ case ANIMATION_FRAME:
+ // currentTime holds the common time for all animations processed
+ // during this frame
+ long currentTime = AnimationUtils.currentAnimationTimeMillis();
+
+ // First, process animations currently sitting on the delayed queue, adding
+ // them to the active animations if they are ready
+ int numDelayedAnims = sDelayedAnims.size();
+ for (int i = 0; i < numDelayedAnims; ++i) {
+ Animator anim = sDelayedAnims.get(i);
+ if (anim.delayedAnimationFrame(currentTime)) {
+ sReadyAnims.add(anim);
+ }
+ }
+ int numReadyAnims = sReadyAnims.size();
+ if (numReadyAnims > 0) {
+ for (int i = 0; i < numReadyAnims; ++i) {
+ Animator anim = sReadyAnims.get(i);
+ anim.startAnimation();
+ sDelayedAnims.remove(anim);
+ }
+ sReadyAnims.clear();
+ }
+
+ // Now process all active animations. The return value from animationFrame()
+ // tells the handler whether it should now be ended
+ int numAnims = sAnimations.size();
+ for (int i = 0; i < numAnims; ++i) {
+ Animator anim = sAnimations.get(i);
+ if (anim.animationFrame(currentTime)) {
+ sEndingAnims.add(anim);
+ }
+ }
+ if (sEndingAnims.size() > 0) {
+ for (int i = 0; i < sEndingAnims.size(); ++i) {
+ sEndingAnims.get(i).endAnimation();
+ }
+ sEndingAnims.clear();
+ }
+
+ // If there are still active or delayed animations, call the handler again
+ // after the frameDelay
+ if (callAgain && (!sAnimations.isEmpty() || !sDelayedAnims.isEmpty())) {
+ sendEmptyMessageDelayed(ANIMATION_FRAME, sFrameDelay);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after
+ * {@link #start()} is called.
+ *
+ * @return the number of milliseconds to delay running the animation
+ */
+ public long getStartDelay() {
+ return mStartDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after
+ * {@link #start()} is called.
+
+ * @param startDelay The amount of the delay, in milliseconds
+ */
+ public void setStartDelay(long startDelay) {
+ this.mStartDelay = startDelay;
+ }
+
+ /**
+ * The amount of time, in milliseconds, between each frame of the animation. This is a
+ * requested time that the animation will attempt to honor, but the actual delay between
+ * frames may be different, depending on system load and capabilities. This is a static
+ * function because the same delay will be applied to all animations, since they are all
+ * run off of a single timing loop.
+ *
+ * @return the requested time between frames, in milliseconds
+ */
+ public static long getFrameDelay() {
+ return sFrameDelay;
+ }
+
+ /**
+ * Gets the value that this animation will start from.
+ *
+ * @return Object The starting value for the animation.
+ */
+ public Object getValueFrom() {
+ return mValueFrom;
+ }
+
+ /**
+ * Sets the value that this animation will start from.
+ */
+ public void setValueFrom(Object valueFrom) {
+ mValueFrom = valueFrom;
+ }
+
+ /**
+ * Gets the value that this animation will animate to.
+ *
+ * @return Object The ending value for the animation.
+ */
+ public Object getValueTo() {
+ return mValueTo;
+ }
+
+ /**
+ * Sets the value that this animation will animate to.
+ *
+ * @return Object The ending value for the animation.
+ */
+ public void setValueTo(Object valueTo) {
+ mValueTo = valueTo;
+ }
+
+ /**
+ * The amount of time, in milliseconds, between each frame of the animation. This is a
+ * requested time that the animation will attempt to honor, but the actual delay between
+ * frames may be different, depending on system load and capabilities. This is a static
+ * function because the same delay will be applied to all animations, since they are all
+ * run off of a single timing loop.
+ *
+ * @param frameDelay the requested time between frames, in milliseconds
+ */
+ public static void setFrameDelay(long frameDelay) {
+ sFrameDelay = frameDelay;
+ }
+
+ /**
+ * The most recent value calculated by this <code>Animator</code> for the property
+ * being animated. This value is only sensible while the animation is running. The main
+ * purpose for this read-only property is to retrieve the value from the <code>Animator</code>
+ * during a call to {@link AnimatorUpdateListener#onAnimationUpdate(Animator)}, which
+ * is called during each animation frame, immediately after the value is calculated.
+ *
+ * @return animatedValue The value most recently calculated by this <code>Animator</code> for
+ * the property specified in the constructor.
+ */
+ public Object getAnimatedValue() {
+ return mAnimatedValue;
+ }
+
+ /**
+ * Sets how many times the animation should be repeated. If the repeat
+ * count is 0, the animation is never repeated. If the repeat count is
+ * greater than 0 or {@link #INFINITE}, the repeat mode will be taken
+ * into account. The repeat count is 0 by default.
+ *
+ * @param value the number of times the animation should be repeated
+ */
+ public void setRepeatCount(int value) {
+ mRepeatCount = value;
+ }
+ /**
+ * Defines how many times the animation should repeat. The default value
+ * is 0.
+ *
+ * @return the number of times the animation should repeat, or {@link #INFINITE}
+ */
+ public int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end. This
+ * setting is applied only when the repeat count is either greater than
+ * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
+ *
+ * @param value {@link #RESTART} or {@link #REVERSE}
+ */
+ public void setRepeatMode(int value) {
+ mRepeatMode = value;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end.
+ *
+ * @return either one of {@link #REVERSE} or {@link #RESTART}
+ */
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent update events through the life of
+ * an animation. This method is called on all listeners for every frame of the animation,
+ * after the values for the animation have been calculated.
+ *
+ * @param listener the listener to be added to the current set of listeners for this animation.
+ */
+ public void addUpdateListener(AnimatorUpdateListener listener) {
+ if (mUpdateListeners == null) {
+ mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
+ }
+ mUpdateListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener from the set listening to frame updates for this animation.
+ *
+ * @param listener the listener to be removed from the current set of update listeners
+ * for this animation.
+ */
+ public void removeUpdateListener(AnimatorUpdateListener listener) {
+ if (mUpdateListeners == null) {
+ return;
+ }
+ mUpdateListeners.remove(listener);
+ if (mUpdateListeners.size() == 0) {
+ mUpdateListeners = null;
+ }
+ }
+
+
+ /**
+ * The time interpolator used in calculating the elapsed fraction of this animation. The
+ * interpolator determines whether the animation runs with linear or non-linear motion,
+ * such as acceleration and deceleration. The default value is
+ * {@link android.view.animation.AccelerateDecelerateInterpolator}
+ *
+ * @param value the interpolator to be used by this animation
+ */
+ public void setInterpolator(Interpolator value) {
+ if (value != null) {
+ mInterpolator = value;
+ }
+ }
+
+ /**
+ * The type evaluator to be used when calculating the animated values of this animation.
+ * The system will automatically assign a float, int, or double evaluator based on the type
+ * of <code>startValue</code> and <code>endValue</code> in the constructor. But if these values
+ * are not one of these primitive types, or if different evaluation is desired (such as is
+ * necessary with int values that represent colors), a custom evaluator needs to be assigned.
+ * For example, when running an animation on color values, the {@link RGBEvaluator}
+ * should be used to get correct RGB color interpolation.
+ *
+ * @param value the evaluator to be used this animation
+ */
+ public void setEvaluator(TypeEvaluator value) {
+ if (value != null) {
+ mEvaluator = value;
+ }
+ }
+
+ public void start() {
+ sPendingAnimations.add(this);
+ if (sAnimationHandler == null) {
+ sAnimationHandler = new AnimationHandler();
+ }
+ // TODO: does this put too many messages on the queue if the handler
+ // is already running?
+ sAnimationHandler.sendEmptyMessage(ANIMATION_START);
+ }
+
+ public void cancel() {
+ if (mListeners != null) {
+ ArrayList<AnimatableListener> tmpListeners =
+ (ArrayList<AnimatableListener>) mListeners.clone();
+ for (AnimatableListener listener : tmpListeners) {
+ listener.onAnimationCancel(this);
+ }
+ }
+ // Just set the CANCELED flag - this causes the animation to end the next time a frame
+ // is processed.
+ mPlayingState = CANCELED;
+ }
+
+ public void end() {
+ // Just set the ENDED flag - this causes the animation to end the next time a frame
+ // is processed.
+ mPlayingState = ENDED;
+ }
+
+ /**
+ * Called internally to end an animation by removing it from the animations list. Must be
+ * called on the UI thread.
+ */
+ private void endAnimation() {
+ sAnimations.remove(this);
+ if (mListeners != null) {
+ ArrayList<AnimatableListener> tmpListeners =
+ (ArrayList<AnimatableListener>) mListeners.clone();
+ for (AnimatableListener listener : tmpListeners) {
+ listener.onAnimationEnd(this);
+ }
+ }
+ mPlayingState = STOPPED;
+ }
+
+ /**
+ * Called internally to start an animation by adding it to the active animations list. Must be
+ * called on the UI thread.
+ */
+ private void startAnimation() {
+ initAnimation();
+ sAnimations.add(this);
+ if (mListeners != null) {
+ ArrayList<AnimatableListener> tmpListeners =
+ (ArrayList<AnimatableListener>) mListeners.clone();
+ for (AnimatableListener listener : tmpListeners) {
+ listener.onAnimationStart(this);
+ }
+ }
+ }
+
+ /**
+ * Internal function called to process an animation frame on an animation that is currently
+ * sleeping through its <code>startDelay</code> phase. The return value indicates whether it
+ * should be woken up and put on the active animations queue.
+ *
+ * @param currentTime The current animation time, used to calculate whether the animation
+ * has exceeded its <code>startDelay</code> and should be started.
+ * @return True if the animation's <code>startDelay</code> has been exceeded and the animation
+ * should be added to the set of active animations.
+ */
+ private boolean delayedAnimationFrame(long currentTime) {
+ if (!mStartedDelay) {
+ mStartedDelay = true;
+ mDelayStartTime = currentTime;
+ } else {
+ long deltaTime = currentTime - mDelayStartTime;
+ if (deltaTime > mStartDelay) {
+ // startDelay ended - start the anim and record the
+ // mStartTime appropriately
+ mStartTime = currentTime - (deltaTime - mStartDelay);
+ mPlayingState = RUNNING;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This internal function processes a single animation frame for a given animation. The
+ * currentTime parameter is the timing pulse sent by the handler, used to calculate the
+ * elapsed duration, and therefore
+ * the elapsed fraction, of the animation. The return value indicates whether the animation
+ * should be ended (which happens when the elapsed time of the animation exceeds the
+ * animation's duration, including the repeatCount).
+ *
+ * @param currentTime The current time, as tracked by the static timing handler
+ * @return true if the animation's duration, including any repetitions due to
+ * <code>repeatCount</code> has been exceeded and the animation should be ended.
+ */
+ private boolean animationFrame(long currentTime) {
+
+ boolean done = false;
+
+ if (mPlayingState == STOPPED) {
+ mPlayingState = RUNNING;
+ mStartTime = currentTime;
+ }
+ switch (mPlayingState) {
+ case RUNNING:
+ float fraction = (float)(currentTime - mStartTime) / mDuration;
+ if (fraction >= 1f) {
+ if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
+ // Time to repeat
+ if (mListeners != null) {
+ for (AnimatableListener listener : mListeners) {
+ listener.onAnimationRepeat(this);
+ }
+ }
+ ++mCurrentIteration;
+ if (mRepeatMode == REVERSE) {
+ mPlayingBackwards = mPlayingBackwards ? false : true;
+ }
+ // TODO: doesn't account for fraction going Wayyyyy over 1, like 2+
+ fraction = fraction - 1f;
+ mStartTime += mDuration;
+ } else {
+ done = true;
+ fraction = Math.min(fraction, 1.0f);
+ }
+ }
+ if (mPlayingBackwards) {
+ fraction = 1f - fraction;
+ }
+ animateValue(fraction);
+ break;
+ case ENDED:
+ // The final value set on the target varies, depending on whether the animation
+ // was supposed to repeat an odd number of times
+ if (mRepeatCount > 0 && (mRepeatCount & 0x01) == 1) {
+ animateValue(0f);
+ } else {
+ animateValue(1f);
+ }
+ // Fall through to set done flag
+ case CANCELED:
+ done = true;
+ break;
+ }
+
+ return done;
+ }
+
+ /**
+ * This method is called with the elapsed fraction of the animation during every
+ * animation frame. This function turns the elapsed fraction into an interpolated fraction
+ * and then into an animated value (from the evaluator. The function is called mostly during
+ * animation updates, but it is also called when the <code>end()</code>
+ * function is called, to set the final value on the property.
+ *
+ * <p>Overrides of this method must call the superclass to perform the calculation
+ * of the animated value.</p>
+ *
+ * @param fraction The elapsed fraction of the animation.
+ */
+ void animateValue(float fraction) {
+ fraction = mInterpolator.getInterpolation(fraction);
+ mAnimatedValue = mEvaluator.evaluate(fraction, mValueFrom, mValueTo);
+ if (mUpdateListeners != null) {
+ int numListeners = mUpdateListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ mUpdateListeners.get(i).onAnimationUpdate(this);
+ }
+ }
+ }
+
+ /**
+ * Implementors of this interface can add themselves as update listeners
+ * to an <code>Animator</code> instance to receive callbacks on every animation
+ * frame, after the current frame's values have been calculated for that
+ * <code>Animator</code>.
+ */
+ public static interface AnimatorUpdateListener {
+ /**
+ * <p>Notifies the occurrence of another frame of the animation.</p>
+ *
+ * @param animation The animation which was repeated.
+ */
+ void onAnimationUpdate(Animator animation);
+
+ }
+} \ No newline at end of file
diff --git a/core/java/android/animation/DoubleEvaluator.java b/core/java/android/animation/DoubleEvaluator.java
new file mode 100644
index 0000000..86e3f22
--- /dev/null
+++ b/core/java/android/animation/DoubleEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>double</code> values.
+ */
+public class DoubleEvaluator implements TypeEvaluator {
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type <code>double</code> or
+ * <code>Double</code>
+ * @param endValue The end value; should be of type <code>double</code> or
+ * <code>Double</code>
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ public Object evaluate(float fraction, Object startValue, Object endValue) {
+ double startDouble = (Double) startValue;
+ return startDouble + fraction * ((Double) endValue - startDouble);
+ }
+} \ No newline at end of file
diff --git a/core/java/android/animation/FloatEvaluator.java b/core/java/android/animation/FloatEvaluator.java
new file mode 100644
index 0000000..29a6f71
--- /dev/null
+++ b/core/java/android/animation/FloatEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>float</code> values.
+ */
+public class FloatEvaluator implements TypeEvaluator {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type <code>float</code> or
+ * <code>Float</code>
+ * @param endValue The end value; should be of type <code>float</code> or <code>Float</code>
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ public Object evaluate(float fraction, Object startValue, Object endValue) {
+ float startFloat = (Float) startValue;
+ return startFloat + fraction * ((Float) endValue - startFloat);
+ }
+} \ No newline at end of file
diff --git a/core/java/android/animation/IntEvaluator.java b/core/java/android/animation/IntEvaluator.java
new file mode 100644
index 0000000..7a2911a
--- /dev/null
+++ b/core/java/android/animation/IntEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>int</code> values.
+ */
+public class IntEvaluator implements TypeEvaluator {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value; should be of type <code>int</code> or
+ * <code>Integer</code>
+ * @param endValue The end value; should be of type <code>int</code> or <code>Integer</code>
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ public Object evaluate(float fraction, Object startValue, Object endValue) {
+ int startInt = (Integer) startValue;
+ return (int) (startInt + fraction * ((Integer) endValue - startInt));
+ }
+} \ No newline at end of file
diff --git a/core/java/android/animation/PropertyAnimator.java b/core/java/android/animation/PropertyAnimator.java
new file mode 100644
index 0000000..99799f0
--- /dev/null
+++ b/core/java/android/animation/PropertyAnimator.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * This subclass of {@link Animator} provides support for animating properties on target objects.
+ * The constructors of this class take parameters to define the target object that will be animated
+ * as well as the name of the property that will be animated. Appropriate set/get functions
+ * are then determined internally and the animation will call these functions as necessary to
+ * animate the property.
+ */
+public final class PropertyAnimator extends Animator {
+
+ // The target object on which the property exists, set in the constructor
+ private Object mTarget;
+
+ private String mPropertyName;
+
+ private Method mGetter = null;
+
+ // The property setter that is assigned internally, based on the propertyName passed into
+ // the constructor
+ private Method mSetter;
+
+ // These maps hold all property entries for a particular class. This map
+ // is used to speed up property/setter/getter lookups for a given class/property
+ // combination. No need to use reflection on the combination more than once.
+ private static final HashMap<Object, HashMap<String, Method>> sSetterPropertyMap =
+ new HashMap<Object, HashMap<String, Method>>();
+ private static final HashMap<Object, HashMap<String, Method>> sGetterPropertyMap =
+ new HashMap<Object, HashMap<String, Method>>();
+
+ // This lock is used to ensure that only one thread is accessing the property maps
+ // at a time.
+ private ReentrantReadWriteLock propertyMapLock = new ReentrantReadWriteLock();
+
+
+ /**
+ * Sets the name of the property that will be animated. This name is used to derive
+ * a setter function that will be called to set animated values.
+ * For example, a property name of <code>foo</code> will result
+ * in a call to the function <code>setFoo()</code> on the target object. If either
+ * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will
+ * also be derived and called.
+ *
+ * <p>Note that the setter function derived from this property name
+ * must take the same parameter type as the
+ * <code>valueFrom</code> and <code>valueTo</code> properties, otherwise the call to
+ * the setter function will fail.</p>
+ *
+ * @param propertyName The name of the property being animated.
+ */
+ public void setPropertyName(String propertyName) {
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * Gets the name of the property that will be animated. This name will be used to derive
+ * a setter function that will be called to set animated values.
+ * For example, a property name of <code>foo</code> will result
+ * in a call to the function <code>setFoo()</code> on the target object. If either
+ * <code>valueFrom</code> or <code>valueTo</code> is null, then a getter function will
+ * also be derived and called.
+ */
+ public String getPropertyName() {
+ return mPropertyName;
+ }
+
+ /**
+ * Sets the <code>Method</code> that is called with the animated values calculated
+ * during the animation. Setting the setter method is an alternative to supplying a
+ * {@link #setPropertyName(String) propertyName} from which the method is derived. This
+ * approach is more direct, and is especially useful when a function must be called that does
+ * not correspond to the convention of <code>setName()</code>. For example, if a function
+ * called <code>offset()</code> is to be called with the animated values, there is no way
+ * to tell <code>PropertyAnimator</code> how to call that function simply through a property
+ * name, so a setter method should be supplied instead.
+ *
+ * <p>Note that the setter function must take the same parameter type as the
+ * <code>valueFrom</code> and <code>valueTo</code> properties, otherwise the call to
+ * the setter function will fail.</p>
+ *
+ * @param setter The setter method that should be called with the animated values.
+ */
+ public void setSetter(Method setter) {
+ mSetter = setter;
+ }
+
+ /**
+ * Gets the <code>Method</code> that is called with the animated values calculated
+ * during the animation.
+ */
+ public Method getSetter() {
+ return mSetter;
+ }
+
+ /**
+ * Sets the <code>Method</code> that is called to get unsupplied <code>valueFrom</code> or
+ * <code>valueTo</code> properties. Setting the getter method is an alternative to supplying a
+ * {@link #setPropertyName(String) propertyName} from which the method is derived. This
+ * approach is more direct, and is especially useful when a function must be called that does
+ * not correspond to the convention of <code>setName()</code>. For example, if a function
+ * called <code>offset()</code> is to be called to get an initial value, there is no way
+ * to tell <code>PropertyAnimator</code> how to call that function simply through a property
+ * name, so a getter method should be supplied instead.
+ *
+ * <p>Note that the getter method is only called whether supplied here or derived
+ * from the property name, if one of <code>valueFrom</code> or <code>valueTo</code> are
+ * null. If both of those values are non-null, then there is no need to get one of the
+ * values and the getter is not called.
+ *
+ * <p>Note that the getter function must return the same parameter type as the
+ * <code>valueFrom</code> and <code>valueTo</code> properties (whichever of them are
+ * non-null), otherwise the call to the getter function will fail.</p>
+ *
+ * @param getter The getter method that should be called to get initial animation values.
+ */
+ public void setGetter(Method getter) {
+ mGetter = getter;
+ }
+
+ /**
+ * Gets the <code>Method</code> that is called to get unsupplied <code>valueFrom</code> or
+ * <code>valueTo</code> properties.
+ */
+ public Method getGetter() {
+ return mGetter;
+ }
+
+ /**
+ * Determine the setter or getter function using the JavaBeans convention of setFoo or
+ * getFoo for a property named 'foo'. This function figures out what the name of the
+ * function should be and uses reflection to find the Method with that name on the
+ * target object.
+ *
+ * @param prefix "set" or "get", depending on whether we need a setter or getter.
+ * @return Method the method associated with mPropertyName.
+ */
+ private Method getPropertyFunction(String prefix) {
+ // TODO: faster implementation...
+ Method returnVal = null;
+ String firstLetter = mPropertyName.substring(0, 1);
+ String theRest = mPropertyName.substring(1);
+ firstLetter = firstLetter.toUpperCase();
+ String setterName = prefix + firstLetter + theRest;
+ Class args[] = new Class[1];
+ args[0] = mValueType;
+ try {
+ returnVal = mTarget.getClass().getMethod(setterName, args);
+ } catch (NoSuchMethodException e) {
+ Log.e("PropertyAnimator",
+ "Couldn't find setter for property " + mPropertyName + ": " + e);
+ }
+ return returnVal;
+ }
+
+ /**
+ * A constructor that takes <code>float</code> values. When this constructor
+ * is called, the system expects to find a setter for <code>propertyName</code> on
+ * the target object that takes a <code>float</code> value.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param target The object whose property is to be animated. This object should
+ * have a public function on it called <code>setName()</code>, where <code>name</code> is
+ * the name of the property passed in as the <code>propertyName</code> parameter.
+ * @param propertyName The name of the property on the <code>target</code> object
+ * that will be animated. Given this name, the constructor will search for a
+ * setter on the target object with the name <code>setPropertyName</code>. For example,
+ * if the constructor is called with <code>propertyName = "foo"</code>, then the
+ * target object should have a setter function with the name <code>setFoo()</code>.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public PropertyAnimator(int duration, Object target, String propertyName,
+ float valueFrom, float valueTo) {
+ super(duration, valueFrom, valueTo);
+ mTarget = target;
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * A constructor that takes <code>int</code> values. When this constructor
+ * is called, the system expects to find a setter for <code>propertyName</code> on
+ * the target object that takes a <code>int</code> value.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param target The object whose property is to be animated. This object should
+ * have a public function on it called <code>setName()</code>, where <code>name</code> is
+ * the name of the property passed in as the <code>propertyName</code> parameter.
+ * @param propertyName The name of the property on the <code>target</code> object
+ * that will be animated. Given this name, the constructor will search for a
+ * setter on the target object with the name <code>setPropertyName</code>. For example,
+ * if the constructor is called with <code>propertyName = "foo"</code>, then the
+ * target object should have a setter function with the name <code>setFoo()</code>.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public PropertyAnimator(int duration, Object target, String propertyName,
+ int valueFrom, int valueTo) {
+ super(duration, valueFrom, valueTo);
+ mTarget = target;
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * A constructor that takes <code>double</code> values. When this constructor
+ * is called, the system expects to find a setter for <code>propertyName</code> on
+ * the target object that takes a <code>double</code> value.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param target The object whose property is to be animated. This object should
+ * have a public function on it called <code>setName()</code>, where <code>name</code> is
+ * the name of the property passed in as the <code>propertyName</code> parameter.
+ * @param propertyName The name of the property on the <code>target</code> object
+ * that will be animated. Given this name, the constructor will search for a
+ * setter on the target object with the name <code>setPropertyName</code>. For example,
+ * if the constructor is called with <code>propertyName = "foo"</code>, then the
+ * target object should have a setter function with the name <code>setFoo()</code>.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public PropertyAnimator(int duration, Object target, String propertyName,
+ double valueFrom, double valueTo) {
+ super(duration, valueFrom, valueTo);
+ mTarget = target;
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * A constructor that takes <code>Object</code> values. When this constructor
+ * is called, the system expects to find a setter for <code>propertyName</code> on
+ * the target object that takes a value of the same type as the <code>Object</code>s.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @param target The object whose property is to be animated. This object should
+ * have a public function on it called <code>setName()</code>, where <code>name</code> is
+ * the name of the property passed in as the <code>propertyName</code> parameter.
+ * @param propertyName The name of the property on the <code>target</code> object
+ * that will be animated. Given this name, the constructor will search for a
+ * setter on the target object with the name <code>setPropertyName</code>. For example,
+ * if the constructor is called with <code>propertyName = "foo"</code>, then the
+ * target object should have a setter function with the name <code>setFoo()</code>.
+ * @param valueFrom The initial value of the property when the animation begins.
+ * @param valueTo The value to which the property will animate.
+ */
+ public PropertyAnimator(int duration, Object target, String propertyName,
+ Object valueFrom, Object valueTo) {
+ super(duration, valueFrom, valueTo);
+ mTarget = target;
+ mPropertyName = propertyName;
+ }
+
+ /**
+ * This function is called immediately before processing the first animation
+ * frame of an animation. If there is a nonzero <code>startDelay</code>, the
+ * function is called after that delay ends.
+ * It takes care of the final initialization steps for the
+ * animation. This includes setting mEvaluator, if the user has not yet
+ * set it up, and the setter/getter methods, if the user did not supply
+ * them.
+ *
+ * <p>Overriders of this method should call the superclass method to cause
+ * internal mechanisms to be set up correctly.</p>
+ */
+ @Override
+ void initAnimation() {
+ super.initAnimation();
+ if (mSetter == null) {
+ try {
+ // Have to lock property map prior to reading it, to guard against
+ // another thread putting something in there after we've checked it
+ // but before we've added an entry to it
+ propertyMapLock.writeLock().lock();
+ HashMap<String, Method> propertyMap = sSetterPropertyMap.get(mTarget);
+ if (propertyMap != null) {
+ mSetter = propertyMap.get(mPropertyName);
+ if (mSetter != null) {
+ return;
+ }
+ }
+ mSetter = getPropertyFunction("set");
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Method>();
+ sSetterPropertyMap.put(mTarget, propertyMap);
+ }
+ propertyMap.put(mPropertyName, mSetter);
+ } finally {
+ propertyMapLock.writeLock().unlock();
+ }
+ }
+ if (getValueFrom() == null || getValueTo() == null) {
+ // Need to set up the getter if not set by the user, then call it
+ // to get the initial values
+ if (mGetter == null) {
+ try {
+ propertyMapLock.writeLock().lock();
+ HashMap<String, Method> propertyMap = sGetterPropertyMap.get(mTarget);
+ if (propertyMap != null) {
+ mGetter = propertyMap.get(mPropertyName);
+ if (mGetter != null) {
+ return;
+ }
+ }
+ mGetter = getPropertyFunction("get");
+ if (propertyMap == null) {
+ propertyMap = new HashMap<String, Method>();
+ sGetterPropertyMap.put(mTarget, propertyMap);
+ }
+ propertyMap.put(mPropertyName, mGetter);
+ } finally {
+ propertyMapLock.writeLock().unlock();
+ }
+ }
+ try {
+ if (getValueFrom() == null) {
+ setValueFrom(mGetter.invoke(mTarget));
+ }
+ if (getValueTo() == null) {
+ setValueTo(mGetter.invoke(mTarget));
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e("PropertyAnimator", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyAnimator", e.toString());
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyAnimator", e.toString());
+ }
+ }
+ }
+
+
+ /**
+ * The target object whose property will be animated by this animation
+ *
+ * @return The object being animated
+ */
+ public Object getTarget() {
+ return mTarget;
+ }
+
+ /**
+ * This method is called with the elapsed fraction of the animation during every
+ * animation frame. This function turns the elapsed fraction into an interpolated fraction
+ * and then into an animated value (from the evaluator. The function is called mostly during
+ * animation updates, but it is also called when the <code>end()</code>
+ * function is called, to set the final value on the property.
+ *
+ * <p>Overrides of this method must call the superclass to perform the calculation
+ * of the animated value.</p>
+ *
+ * @param fraction The elapsed fraction of the animation.
+ */
+ @Override
+ void animateValue(float fraction) {
+ super.animateValue(fraction);
+ if (mSetter != null) {
+ try {
+ mSetter.invoke(mTarget, getAnimatedValue());
+ } catch (InvocationTargetException e) {
+ Log.e("PropertyAnimator", e.toString());
+ } catch (IllegalAccessException e) {
+ Log.e("PropertyAnimator", e.toString());
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Animator: target: " + this.mTarget + "\n" +
+ " property: " + mPropertyName + "\n" +
+ " from: " + getValueFrom() + "\n" +
+ " to: " + getValueTo();
+ }
+}
diff --git a/core/java/android/animation/RGBEvaluator.java b/core/java/android/animation/RGBEvaluator.java
new file mode 100644
index 0000000..bae0af0
--- /dev/null
+++ b/core/java/android/animation/RGBEvaluator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * This evaluator can be used to perform type interpolation between integer
+ * values that represent ARGB colors.
+ */
+public class RGBEvaluator implements TypeEvaluator {
+
+ /**
+ * This function returns the calculated in-between value for a color
+ * given integers that represent the start and end values in the four
+ * bytes of the 32-bit int. Each channel is separately linearly interpolated
+ * and the resulting calculated values are recombined into the return value.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue A 32-bit int value representing colors in the
+ * separate bytes of the parameter
+ * @param endValue A 32-bit int value representing colors in the
+ * separate bytes of the parameter
+ * @return A value that is calculated to be the linearly interpolated
+ * result, derived by separating the start and end values into separate
+ * color channels and interpolating each one separately, recombining the
+ * resulting values in the same way.
+ */
+ public Object evaluate(float fraction, Object startValue, Object endValue) {
+ int startInt = (Integer) startValue;
+ int startA = (startInt >> 24);
+ int startR = (startInt >> 16) & 0xff;
+ int startG = (startInt >> 8) & 0xff;
+ int startB = startInt & 0xff;
+
+ int endInt = (Integer) endValue;
+ int endA = (endInt >> 24);
+ int endR = (endInt >> 16) & 0xff;
+ int endG = (endInt >> 8) & 0xff;
+ int endB = endInt & 0xff;
+
+ return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
+ (int)((startR + (int)(fraction * (endR - startR))) << 16) |
+ (int)((startG + (int)(fraction * (endG - startG))) << 8) |
+ (int)((startB + (int)(fraction * (endB - startB))));
+ }
+} \ No newline at end of file
diff --git a/core/java/android/animation/Sequencer.java b/core/java/android/animation/Sequencer.java
new file mode 100644
index 0000000..00ab6a3
--- /dev/null
+++ b/core/java/android/animation/Sequencer.java
@@ -0,0 +1,681 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * This class plays a set of {@link Animatable} objects in the specified order. Animations
+ * can be set up to play together, in sequence, or after a specified delay.
+ *
+ * <p>There are two different approaches to adding animations to a <code>Sequencer</code>:
+ * either the {@link Sequencer#playTogether(Animatable...) playTogether()} or
+ * {@link Sequencer#playSequentially(Animatable...) playSequentially()} methods can be called to add
+ * a set of animations all at once, or the {@link Sequencer#play(Animatable)} can be
+ * used in conjunction with methods in the {@link android.animation.Sequencer.Builder Builder}
+ * class to add animations
+ * one by one.</p>
+ *
+ * <p>It is possible to set up a <code>Sequencer</code> with circular dependencies between
+ * its animations. For example, an animation a1 could be set up to start before animation a2, a2
+ * before a3, and a3 before a1. The results of this configuration are undefined, but will typically
+ * result in none of the affected animations being played. Because of this (and because
+ * circular dependencies do not make logical sense anyway), circular dependencies
+ * should be avoided, and the dependency flow of animations should only be in one direction.
+ */
+public final class Sequencer extends Animatable {
+
+ /**
+ * Tracks aniamtions currently being played, so that we know what to
+ * cancel or end when cancel() or end() is called on this Sequencer
+ */
+ private final ArrayList<Animatable> mPlayingSet = new ArrayList<Animatable>();
+
+ /**
+ * Contains all nodes, mapped to their respective Animatables. When new
+ * dependency information is added for an Animatable, we want to add it
+ * to a single node representing that Animatable, not create a new Node
+ * if one already exists.
+ */
+ private final HashMap<Animatable, Node> mNodeMap = new HashMap<Animatable, Node>();
+
+ /**
+ * Set of all nodes created for this Sequencer. This list is used upon
+ * starting the sequencer, and the nodes are placed in sorted order into the
+ * sortedNodes collection.
+ */
+ private final ArrayList<Node> mNodes = new ArrayList<Node>();
+
+ /**
+ * The sorted list of nodes. This is the order in which the animations will
+ * be played. The details about when exactly they will be played depend
+ * on the dependency relationships of the nodes.
+ */
+ private final ArrayList<Node> mSortedNodes = new ArrayList<Node>();
+
+ /**
+ * The set of listeners to be sent events through the life of an animation.
+ */
+ private ArrayList<AnimatableListener> mListeners = null;
+
+ /**
+ * Flag indicating whether the nodes should be sorted prior to playing. This
+ * flag allows us to cache the previous sorted nodes so that if the sequence
+ * is replayed with no changes, it does not have to re-sort the nodes again.
+ */
+ private boolean mNeedsSort = true;
+
+ private SequencerAnimatableListener mSequenceListener = null;
+
+ /**
+ * Sets up this Sequencer to play all of the supplied animations at the same time.
+ *
+ * @param sequenceItems The animations that will be started simultaneously.
+ */
+ public void playTogether(Animatable... sequenceItems) {
+ if (sequenceItems != null) {
+ mNeedsSort = true;
+ Builder builder = play(sequenceItems[0]);
+ for (int i = 1; i < sequenceItems.length; ++i) {
+ builder.with(sequenceItems[i]);
+ }
+ }
+ }
+
+ /**
+ * Sets up this Sequencer to play each of the supplied animations when the
+ * previous animation ends.
+ *
+ * @param sequenceItems The aniamtions that will be started one after another.
+ */
+ public void playSequentially(Animatable... sequenceItems) {
+ if (sequenceItems != null) {
+ mNeedsSort = true;
+ if (sequenceItems.length == 1) {
+ play(sequenceItems[0]);
+ } else {
+ for (int i = 0; i < sequenceItems.length - 1; ++i) {
+ play(sequenceItems[i]).before(sequenceItems[i+1]);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method creates a <code>Builder</code> object, which is used to
+ * set up playing constraints. This initial <code>play()</code> method
+ * tells the <code>Builder</code> the animation that is the dependency for
+ * the succeeding commands to the <code>Builder</code>. For example,
+ * calling <code>play(a1).with(a2)</code> sets up the Sequence to play
+ * <code>a1</code> and <code>a2</code> at the same time,
+ * <code>play(a1).before(a2)</code> sets up the Sequence to play
+ * <code>a1</code> first, followed by <code>a2</code>, and
+ * <code>play(a1).after(a2)</code> sets up the Sequence to play
+ * <code>a2</code> first, followed by <code>a1</code>.
+ *
+ * <p>Note that <code>play()</code> is the only way to tell the
+ * <code>Builder</code> the animation upon which the dependency is created,
+ * so successive calls to the various functions in <code>Builder</code>
+ * will all refer to the initial parameter supplied in <code>play()</code>
+ * as the dependency of the other animations. For example, calling
+ * <code>play(a1).before(a2).before(a3)</code> will play both <code>a2</code>
+ * and <code>a3</code> when a1 ends; it does not set up a dependency between
+ * <code>a2</code> and <code>a3</code>.</p>
+ *
+ * @param anim The animation that is the dependency used in later calls to the
+ * methods in the returned <code>Builder</code> object. A null parameter will result
+ * in a null <code>Builder</code> return value.
+ * @return Builder The object that constructs the sequence based on the dependencies
+ * outlined in the calls to <code>play</code> and the other methods in the
+ * <code>Builder</code object.
+ */
+ public Builder play(Animatable anim) {
+ if (anim != null) {
+ mNeedsSort = true;
+ return new Builder(anim);
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Note that canceling a <code>Sequencer</code> also cancels all of the animations that it is
+ * responsible for.</p>
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void cancel() {
+ if (mListeners != null) {
+ ArrayList<AnimatableListener> tmpListeners =
+ (ArrayList<AnimatableListener>) mListeners.clone();
+ for (AnimatableListener listener : tmpListeners) {
+ listener.onAnimationCancel(this);
+ }
+ }
+ if (mPlayingSet.size() > 0) {
+ for (Animatable item : mPlayingSet) {
+ item.cancel();
+ }
+ mPlayingSet.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Note that ending a <code>Sequencer</code> also ends all of the animations that it is
+ * responsible for.</p>
+ */
+ @Override
+ public void end() {
+ if (mPlayingSet.size() > 0) {
+ for (Animatable item : mPlayingSet) {
+ item.end();
+ }
+ mPlayingSet.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Starting this <code>Sequencer</code> will, in turn, start the animations for which
+ * it is responsible. The details of when exactly those animations are started depends on
+ * the dependency relationships that have been set up between the animations.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void start() {
+ // First, sort the nodes (if necessary). This will ensure that sortedNodes
+ // contains the animation nodes in the correct order.
+ sortNodes();
+
+ // nodesToStart holds the list of nodes to be started immediately. We don't want to
+ // start the animations in the loop directly because we first need to set up
+ // dependencies on all of the nodes. For example, we don't want to start an animation
+ // when some other animation also wants to start when the first animation begins.
+ ArrayList<Node> nodesToStart = new ArrayList<Node>();
+ for (Node node : mSortedNodes) {
+ if (mSequenceListener == null) {
+ mSequenceListener = new SequencerAnimatableListener(this);
+ }
+ node.animation.addListener(mSequenceListener);
+ if (node.dependencies == null || node.dependencies.size() == 0) {
+ nodesToStart.add(node);
+ } else {
+ for (Dependency dependency : node.dependencies) {
+ dependency.node.animation.addListener(
+ new DependencyListener(node, dependency.rule));
+ }
+ node.tmpDependencies = (ArrayList<Dependency>) node.dependencies.clone();
+ }
+ }
+ // Now that all dependencies are set up, start the animations that should be started.
+ for (Node node : nodesToStart) {
+ node.animation.start();
+ mPlayingSet.add(node.animation);
+ }
+ if (mListeners != null) {
+ ArrayList<AnimatableListener> tmpListeners =
+ (ArrayList<AnimatableListener>) mListeners.clone();
+ for (AnimatableListener listener : tmpListeners) {
+ listener.onAnimationStart(this);
+ }
+ }
+ }
+
+ /**
+ * This class is the mechanism by which animations are started based on events in other
+ * animations. If an animation has multiple dependencies on other animations, then
+ * all dependencies must be satisfied before the animation is started.
+ */
+ private static class DependencyListener implements AnimatableListener {
+
+ // The node upon which the dependency is based.
+ private Node mNode;
+
+ // The Dependency rule (WITH or AFTER) that the listener should wait for on
+ // the node
+ private int mRule;
+
+ public DependencyListener(Node node, int rule) {
+ this.mNode = node;
+ this.mRule = rule;
+ }
+
+ /**
+ * If an animation that is being listened for is canceled, then this removes
+ * the listener on that animation, to avoid triggering further animations down
+ * the line when the animation ends.
+ */
+ public void onAnimationCancel(Animatable animation) {
+ Dependency dependencyToRemove = null;
+ for (Dependency dependency : mNode.tmpDependencies) {
+ if (dependency.node.animation == animation) {
+ // animation canceled - remove the dependency and listener
+ dependencyToRemove = dependency;
+ animation.removeListener(this);
+ break;
+ }
+ }
+ mNode.tmpDependencies.remove(dependencyToRemove);
+ }
+
+ /**
+ * An end event is received - see if this is an event we are listening for
+ */
+ public void onAnimationEnd(Animatable animation) {
+ if (mRule == Dependency.AFTER) {
+ startIfReady(animation);
+ }
+ }
+
+ /**
+ * Ignore repeat events for now
+ */
+ public void onAnimationRepeat(Animatable animation) {
+ }
+
+ /**
+ * A start event is received - see if this is an event we are listening for
+ */
+ public void onAnimationStart(Animatable animation) {
+ if (mRule == Dependency.WITH) {
+ startIfReady(animation);
+ }
+ }
+
+ /**
+ * Check whether the event received is one that the node was waiting for.
+ * If so, mark it as complete and see whether it's time to start
+ * the animation.
+ * @param dependencyAnimation the animation that sent the event.
+ */
+ private void startIfReady(Animatable dependencyAnimation) {
+ Dependency dependencyToRemove = null;
+ for (Dependency dependency : mNode.tmpDependencies) {
+ if (dependency.rule == mRule &&
+ dependency.node.animation == dependencyAnimation) {
+ // rule fired - remove the dependency and listener and check to
+ // see whether it's time to start the animation
+ dependencyToRemove = dependency;
+ dependencyAnimation.removeListener(this);
+ break;
+ }
+ }
+ mNode.tmpDependencies.remove(dependencyToRemove);
+ if (mNode.tmpDependencies.size() == 0) {
+ // all dependencies satisfied: start the animation
+ mNode.animation.start();
+ }
+ }
+
+ }
+
+ private class SequencerAnimatableListener implements AnimatableListener {
+
+ private Sequencer mSequencer;
+
+ SequencerAnimatableListener(Sequencer sequencer) {
+ mSequencer = sequencer;
+ }
+
+ public void onAnimationCancel(Animatable animation) {
+ if (mPlayingSet.size() == 0) {
+ if (mListeners != null) {
+ for (AnimatableListener listener : mListeners) {
+ listener.onAnimationCancel(mSequencer);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public void onAnimationEnd(Animatable animation) {
+ animation.removeListener(this);
+ mPlayingSet.remove(animation);
+ if (mPlayingSet.size() == 0) {
+ // If this was the last child animation to end, then notify listeners that this
+ // sequence ended
+ if (mListeners != null) {
+ ArrayList<AnimatableListener> tmpListeners =
+ (ArrayList<AnimatableListener>) mListeners.clone();
+ for (AnimatableListener listener : tmpListeners) {
+ listener.onAnimationEnd(mSequencer);
+ }
+ }
+ }
+ }
+
+ // Nothing to do
+ public void onAnimationRepeat(Animatable animation) {
+ }
+
+ // Nothing to do
+ public void onAnimationStart(Animatable animation) {
+ }
+
+ }
+
+ /**
+ * This method sorts the current set of nodes, if needed. The sort is a simple
+ * DependencyGraph sort, which goes like this:
+ * - All nodes without dependencies become 'roots'
+ * - while roots list is not null
+ * - for each root r
+ * - add r to sorted list
+ * - remove r as a dependency from any other node
+ * - any nodes with no dependencies are added to the roots list
+ */
+ private void sortNodes() {
+ if (mNeedsSort) {
+ mSortedNodes.clear();
+ ArrayList<Node> roots = new ArrayList<Node>();
+ for (Node node : mNodes) {
+ if (node.dependencies == null || node.dependencies.size() == 0) {
+ roots.add(node);
+ }
+ }
+ ArrayList<Node> tmpRoots = new ArrayList<Node>();
+ while (roots.size() > 0) {
+ for (Node root : roots) {
+ mSortedNodes.add(root);
+ if (root.nodeDependents != null) {
+ for (Node node : root.nodeDependents) {
+ node.nodeDependencies.remove(root);
+ if (node.nodeDependencies.size() == 0) {
+ tmpRoots.add(node);
+ }
+ }
+ }
+ }
+ roots.addAll(tmpRoots);
+ tmpRoots.clear();
+ }
+ mNeedsSort = false;
+ if (mSortedNodes.size() != mNodes.size()) {
+ throw new IllegalStateException("Circular dependencies cannot exist"
+ + " in Sequencer");
+ }
+ } else {
+ // Doesn't need sorting, but still need to add in the nodeDependencies list
+ // because these get removed as the event listeners fire and the dependencies
+ // are satisfied
+ for (Node node : mNodes) {
+ if (node.dependencies != null && node.dependencies.size() > 0) {
+ for (Dependency dependency : node.dependencies) {
+ if (node.nodeDependencies == null) {
+ node.nodeDependencies = new ArrayList<Node>();
+ }
+ if (!node.nodeDependencies.contains(dependency.node)) {
+ node.nodeDependencies.add(dependency.node);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Dependency holds information about the node that some other node is
+ * dependent upon and the nature of that dependency.
+ *
+ */
+ private static class Dependency {
+ static final int WITH = 0; // dependent node must start with this dependency node
+ static final int AFTER = 1; // dependent node must start when this dependency node finishes
+
+ // The node that the other node with this Dependency is dependent upon
+ public Node node;
+
+ // The nature of the dependency (WITH or AFTER)
+ public int rule;
+
+ public Dependency(Node node, int rule) {
+ this.node = node;
+ this.rule = rule;
+ }
+ }
+
+ /**
+ * A Node is an embodiment of both the Animatable that it wraps as well as
+ * any dependencies that are associated with that Animation. This includes
+ * both dependencies upon other nodes (in the dependencies list) as
+ * well as dependencies of other nodes upon this (in the nodeDependents list).
+ */
+ private static class Node {
+ public Animatable animation;
+
+ /**
+ * These are the dependencies that this node's animation has on other
+ * nodes. For example, if this node's animation should begin with some
+ * other animation ends, then there will be an item in this node's
+ * dependencies list for that other animation's node.
+ */
+ public ArrayList<Dependency> dependencies = null;
+
+ /**
+ * tmpDependencies is a runtime detail. We use the dependencies list for sorting.
+ * But we also use the list to keep track of when multiple dependencies are satisfied,
+ * but removing each dependency as it is satisfied. We do not want to remove
+ * the dependency itself from the list, because we need to retain that information
+ * if the sequencer is launched in the future. So we create a copy of the dependency
+ * list when the sequencer starts and use this tmpDependencies list to track the
+ * list of satisfied dependencies.
+ */
+ public ArrayList<Dependency> tmpDependencies = null;
+
+ /**
+ * nodeDependencies is just a list of the nodes that this Node is dependent upon.
+ * This information is used in sortNodes(), to determine when a node is a root.
+ */
+ public ArrayList<Node> nodeDependencies = null;
+
+ /**
+ * nodeDepdendents is the list of nodes that have this node as a dependency. This
+ * is a utility field used in sortNodes to facilitate removing this node as a
+ * dependency when it is a root node.
+ */
+ public ArrayList<Node> nodeDependents = null;
+
+ /**
+ * Constructs the Node with the animation that it encapsulates. A Node has no
+ * dependencies by default; dependencies are added via the addDependency()
+ * method.
+ *
+ * @param animation The animation that the Node encapsulates.
+ */
+ public Node(Animatable animation) {
+ this.animation = animation;
+ }
+
+ /**
+ * Add a dependency to this Node. The dependency includes information about the
+ * node that this node is dependency upon and the nature of the dependency.
+ * @param dependency
+ */
+ public void addDependency(Dependency dependency) {
+ if (dependencies == null) {
+ dependencies = new ArrayList<Dependency>();
+ nodeDependencies = new ArrayList<Node>();
+ }
+ dependencies.add(dependency);
+ if (!nodeDependencies.contains(dependency.node)) {
+ nodeDependencies.add(dependency.node);
+ }
+ Node dependencyNode = dependency.node;
+ if (dependencyNode.nodeDependents == null) {
+ dependencyNode.nodeDependents = new ArrayList<Node>();
+ }
+ dependencyNode.nodeDependents.add(this);
+ }
+ }
+
+ /**
+ * The <code>Builder</code> object is a utility class to facilitate adding animations to a
+ * <code>Sequencer</code> along with the relationships between the various animations. The
+ * intention of the <code>Builder</code> methods, along with the {@link
+ * Sequencer#play(Animatable) play()} method of <code>Sequencer</code> is to make it possible to
+ * express the dependency relationships of animations in a natural way. Developers can also use
+ * the {@link Sequencer#playTogether(Animatable...) playTogether()} and {@link
+ * Sequencer#playSequentially(Animatable...) playSequentially()} methods if these suit the need,
+ * but it might be easier in some situations to express the sequence of animations in pairs.
+ * <p/>
+ * <p>The <code>Builder</code> object cannot be constructed directly, but is rather constructed
+ * internally via a call to {@link Sequencer#play(Animatable)}.</p>
+ * <p/>
+ * <p>For example, this sets up a Sequencer to play anim1 and anim2 at the same time, anim3 to
+ * play when anim2 finishes, and anim4 to play when anim3 finishes:</p>
+ * <pre>
+ * Sequencer s = new Sequencer();
+ * s.play(anim1).with(anim2);
+ * s.play(anim2).before(anim3);
+ * s.play(anim4).after(anim3);
+ * </pre>
+ * <p/>
+ * <p>Note in the example that both {@link Builder#before(Animatable)} and {@link
+ * Builder#after(Animatable)} are used. These are just different ways of expressing the same
+ * relationship and are provided to make it easier to say things in a way that is more natural,
+ * depending on the situation.</p>
+ * <p/>
+ * <p>It is possible to make several calls into the same <code>Builder</code> object to express
+ * multiple relationships. However, note that it is only the animation passed into the initial
+ * {@link Sequencer#play(Animatable)} method that is the dependency in any of the successive
+ * calls to the <code>Builder</code> object. For example, the following code starts both anim2
+ * and anim3 when anim1 ends; there is no direct dependency relationship between anim2 and
+ * anim3:
+ * <pre>
+ * Sequencer s = new Sequencer();
+ * s.play(anim1).before(anim2).before(anim3);
+ * </pre>
+ * If the desired result is to play anim1 then anim2 then anim3, this code expresses the
+ * relationship correctly:</p>
+ * <pre>
+ * Sequencer s = new Sequencer();
+ * s.play(anim1).before(anim2);
+ * s.play(anim2).before(anim3);
+ * </pre>
+ * <p/>
+ * <p>Note that it is possible to express relationships that cannot be resolved and will not
+ * result in sensible results. For example, <code>play(anim1).after(anim1)</code> makes no
+ * sense. In general, circular dependencies like this one (or more indirect ones where a depends
+ * on b, which depends on c, which depends on a) should be avoided. Only create sequences that
+ * can boil down to a simple, one-way relationship of animations starting with, before, and
+ * after other, different, animations.</p>
+ */
+ public class Builder {
+
+ /**
+ * This tracks the current node being processed. It is supplied to the play() method
+ * of Sequencer and passed into the constructor of Builder.
+ */
+ private Node mCurrentNode;
+
+ /**
+ * package-private constructor. Builders are only constructed by Sequencer, when the
+ * play() method is called.
+ *
+ * @param anim The animation that is the dependency for the other animations passed into
+ * the other methods of this Builder object.
+ */
+ Builder(Animatable anim) {
+ mCurrentNode = mNodeMap.get(anim);
+ if (mCurrentNode == null) {
+ mCurrentNode = new Node(anim);
+ mNodeMap.put(anim, mCurrentNode);
+ mNodes.add(mCurrentNode);
+ }
+ }
+
+ /**
+ * Sets up the given animation to play at the same time as the animation supplied in the
+ * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object.
+ *
+ * @param anim The animation that will play when the animation supplied to the
+ * {@link Sequencer#play(Animatable)} method starts.
+ */
+ public void with(Animatable anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH);
+ node.addDependency(dependency);
+ }
+
+ /**
+ * Sets up the given animation to play when the animation supplied in the
+ * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object
+ * ends.
+ *
+ * @param anim The animation that will play when the animation supplied to the
+ * {@link Sequencer#play(Animatable)} method ends.
+ */
+ public void before(Animatable anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER);
+ node.addDependency(dependency);
+ }
+
+ /**
+ * Sets up the given animation to play when the animation supplied in the
+ * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object
+ * to start when the animation supplied in this method call ends.
+ *
+ * @param anim The animation whose end will cause the animation supplied to the
+ * {@link Sequencer#play(Animatable)} method to play.
+ */
+ public void after(Animatable anim) {
+ Node node = mNodeMap.get(anim);
+ if (node == null) {
+ node = new Node(anim);
+ mNodeMap.put(anim, node);
+ mNodes.add(node);
+ }
+ Dependency dependency = new Dependency(node, Dependency.AFTER);
+ mCurrentNode.addDependency(dependency);
+ }
+
+ /**
+ * Sets up the animation supplied in the
+ * {@link Sequencer#play(Animatable)} call that created this <code>Builder</code> object
+ * to play when the given amount of time elapses.
+ *
+ * @param delay The number of milliseconds that should elapse before the
+ * animation starts.
+ */
+ public void after(long delay) {
+ // setup dummy Animator just to run the clock
+ Animator anim = new Animator(delay, 0, 1);
+ Node node = new Node(anim);
+ mNodes.add(node);
+ Dependency dependency = new Dependency(node, Dependency.AFTER);
+ mCurrentNode.addDependency(dependency);
+ }
+
+ }
+
+}
diff --git a/core/java/android/animation/TypeEvaluator.java b/core/java/android/animation/TypeEvaluator.java
new file mode 100644
index 0000000..6150e00
--- /dev/null
+++ b/core/java/android/animation/TypeEvaluator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation;
+
+/**
+ * Interface for use with the {@link Animator#setEvaluator(TypeEvaluator)} function. Evaluators
+ * allow developers to create animations on arbitrary property types, by allowing them to supply
+ * custom evaulators for types that are not automatically understood and used by the animation
+ * system.
+ *
+ * @see Animator#setEvaluator(TypeEvaluator)
+ */
+public interface TypeEvaluator {
+
+ /**
+ * This function returns the result of linearly interpolating the start and end values, with
+ * <code>fraction</code> representing the proportion between the start and end values. The
+ * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
+ * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
+ * and <code>t</code> is <code>fraction</code>.
+ *
+ * @param fraction The fraction from the starting to the ending values
+ * @param startValue The start value.
+ * @param endValue The end value.
+ * @return A linear interpolation between the start and end values, given the
+ * <code>fraction</code> parameter.
+ */
+ public Object evaluate(float fraction, Object startValue, Object endValue);
+
+} \ No newline at end of file
diff --git a/core/java/android/animation/package.html b/core/java/android/animation/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/core/java/android/animation/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+ {@hide}
+</body>
+</html>
diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java
new file mode 100644
index 0000000..3cd2b9e
--- /dev/null
+++ b/core/java/android/app/ActionBar.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.graphics.drawable.Drawable;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.SpinnerAdapter;
+
+/**
+ * This is the public interface to the contextual ActionBar.
+ * The ActionBar acts as a replacement for the title bar in Activities.
+ * It provides facilities for creating toolbar actions as well as
+ * methods of navigating around an application.
+ */
+public abstract class ActionBar {
+ /**
+ * Standard navigation mode. Consists of either a logo or icon
+ * and title text with an optional subtitle. Clicking any of these elements
+ * will dispatch onActionItemSelected to the registered Callback with
+ * a MenuItem with item ID android.R.id.home.
+ */
+ public static final int NAVIGATION_MODE_STANDARD = 0;
+
+ /**
+ * Dropdown list navigation mode. Instead of static title text this mode
+ * presents a dropdown menu for navigation within the activity.
+ */
+ public static final int NAVIGATION_MODE_DROPDOWN_LIST = 1;
+
+ /**
+ * Tab navigation mode. Instead of static title text this mode
+ * presents a series of tabs for navigation within the activity.
+ */
+ public static final int NAVIGATION_MODE_TABS = 2;
+
+ /**
+ * Custom navigation mode. This navigation mode is set implicitly whenever
+ * a custom navigation view is set. See {@link #setCustomNavigationMode(View)}.
+ */
+ public static final int NAVIGATION_MODE_CUSTOM = 3;
+
+ /**
+ * Use logo instead of icon if available. This flag will cause appropriate
+ * navigation modes to use a wider logo in place of the standard icon.
+ */
+ public static final int DISPLAY_USE_LOGO = 0x1;
+
+ /**
+ * Hide 'home' elements in this action bar, leaving more space for other
+ * navigation elements. This includes logo and icon.
+ */
+ public static final int DISPLAY_HIDE_HOME = 0x2;
+
+ /**
+ * Set the action bar into custom navigation mode, supplying a view
+ * for custom navigation.
+ *
+ * Custom navigation views appear between the application icon and
+ * any action buttons and may use any space available there. Common
+ * use cases for custom navigation views might include an auto-suggesting
+ * address bar for a browser or other navigation mechanisms that do not
+ * translate well to provided navigation modes.
+ *
+ * @param view Custom navigation view to place in the ActionBar.
+ */
+ public abstract void setCustomNavigationMode(View view);
+
+ /**
+ * Set the action bar into dropdown navigation mode and supply an adapter
+ * that will provide views for navigation choices.
+ *
+ * @param adapter An adapter that will provide views both to display
+ * the current navigation selection and populate views
+ * within the dropdown navigation menu.
+ * @param callback A NavigationCallback that will receive events when the user
+ * selects a navigation item.
+ */
+ public abstract void setDropdownNavigationMode(SpinnerAdapter adapter,
+ NavigationCallback callback);
+
+ /**
+ * Set the action bar into standard navigation mode, supplying a title and subtitle.
+ *
+ * Standard navigation mode is default. The title is automatically set to the
+ * name of your Activity. Subtitles are displayed underneath the title, usually
+ * in a smaller font or otherwise less prominently than the title. Subtitles are
+ * good for extended descriptions of activity state.
+ *
+ * @param title The action bar's title. null is treated as an empty string.
+ * @param subtitle The action bar's subtitle. null will remove the subtitle entirely.
+ */
+ public abstract void setStandardNavigationMode(CharSequence title, CharSequence subtitle);
+
+ /**
+ * Set the action bar into standard navigation mode, supplying a title and subtitle.
+ *
+ * Standard navigation mode is default. The title is automatically set to the
+ * name of your Activity on startup if an action bar is present.
+ *
+ * @param title The action bar's title. null is treated as an empty string.
+ */
+ public abstract void setStandardNavigationMode(CharSequence title);
+
+ /**
+ * Set the action bar into standard navigation mode, using the currently set title
+ * and/or subtitle.
+ *
+ * Standard navigation mode is default. The title is automatically set to the name of
+ * your Activity on startup if an action bar is present.
+ */
+ public abstract void setStandardNavigationMode();
+
+ /**
+ * Set the action bar's title. This will only be displayed in standard navigation mode.
+ *
+ * @param title Title to set
+ */
+ public abstract void setTitle(CharSequence title);
+
+ /**
+ * Set the action bar's subtitle. This will only be displayed in standard navigation mode.
+ * Set to null to disable the subtitle entirely.
+ *
+ * @param subtitle Subtitle to set
+ */
+ public abstract void setSubtitle(CharSequence subtitle);
+
+ /**
+ * Set display options. This changes all display option bits at once. To change
+ * a limited subset of display options, see {@link #setDisplayOptions(int, int)}.
+ *
+ * @param options A combination of the bits defined by the DISPLAY_ constants
+ * defined in ActionBar.
+ */
+ public abstract void setDisplayOptions(int options);
+
+ /**
+ * Set selected display options. Only the options specified by mask will be changed.
+ * To change all display option bits at once, see {@link #setDisplayOptions(int)}.
+ *
+ * <p>Example: setDisplayOptions(0, DISPLAY_HIDE_HOME) will disable the
+ * {@link #DISPLAY_HIDE_HOME} option.
+ * setDisplayOptions(DISPLAY_HIDE_HOME, DISPLAY_HIDE_HOME | DISPLAY_USE_LOGO)
+ * will enable {@link #DISPLAY_HIDE_HOME} and disable {@link #DISPLAY_USE_LOGO}.
+ *
+ * @param options A combination of the bits defined by the DISPLAY_ constants
+ * defined in ActionBar.
+ * @param mask A bit mask declaring which display options should be changed.
+ */
+ public abstract void setDisplayOptions(int options, int mask);
+
+ /**
+ * Set the ActionBar's background.
+ *
+ * @param d Background drawable
+ */
+ public abstract void setBackgroundDrawable(Drawable d);
+
+ /**
+ * @return The current custom navigation view.
+ */
+ public abstract View getCustomNavigationView();
+
+ /**
+ * Returns the current ActionBar title in standard mode.
+ * Returns null if {@link #getNavigationMode()} would not return
+ * {@link #NAVIGATION_MODE_STANDARD}.
+ *
+ * @return The current ActionBar title or null.
+ */
+ public abstract CharSequence getTitle();
+
+ /**
+ * Returns the current ActionBar subtitle in standard mode.
+ * Returns null if {@link #getNavigationMode()} would not return
+ * {@link #NAVIGATION_MODE_STANDARD}.
+ *
+ * @return The current ActionBar subtitle or null.
+ */
+ public abstract CharSequence getSubtitle();
+
+ /**
+ * Returns the current navigation mode. The result will be one of:
+ * <ul>
+ * <li>{@link #NAVIGATION_MODE_STANDARD}</li>
+ * <li>{@link #NAVIGATION_MODE_DROPDOWN_LIST}</li>
+ * <li>{@link #NAVIGATION_MODE_TABS}</li>
+ * <li>{@link #NAVIGATION_MODE_CUSTOM}</li>
+ * </ul>
+ *
+ * @return The current navigation mode.
+ *
+ * @see #setStandardNavigationMode()
+ * @see #setStandardNavigationMode(CharSequence)
+ * @see #setStandardNavigationMode(CharSequence, CharSequence)
+ * @see #setDropdownNavigationMode(SpinnerAdapter)
+ * @see #setTabNavigationMode()
+ * @see #setCustomNavigationMode(View)
+ */
+ public abstract int getNavigationMode();
+
+ /**
+ * @return The current set of display options.
+ */
+ public abstract int getDisplayOptions();
+
+ /**
+ * Start a context mode controlled by <code>callback</code>.
+ * The {@link ContextModeCallback} will receive lifecycle events for the duration
+ * of the context mode.
+ *
+ * @param callback Callback handler that will manage this context mode.
+ */
+ public abstract void startContextMode(ContextModeCallback callback);
+
+ /**
+ * Finish the current context mode.
+ */
+ public abstract void finishContextMode();
+
+ /**
+ * Set the action bar into tabbed navigation mode.
+ *
+ * @see #addTab(Tab)
+ * @see #insertTab(Tab, int)
+ * @see #removeTab(Tab)
+ * @see #removeTabAt(int)
+ */
+ public abstract void setTabNavigationMode();
+
+ /**
+ * Set the action bar into tabbed navigation mode.
+ *
+ * @param containerViewId Id of the container view where tab content fragments should appear.
+ *
+ * @see #addTab(Tab)
+ * @see #insertTab(Tab, int)
+ * @see #removeTab(Tab)
+ * @see #removeTabAt(int)
+ */
+ public abstract void setTabNavigationMode(int containerViewId);
+
+ /**
+ * Create and return a new {@link Tab}.
+ * This tab will not be included in the action bar until it is added.
+ *
+ * @return A new Tab
+ *
+ * @see #addTab(Tab)
+ * @see #insertTab(Tab, int)
+ */
+ public abstract Tab newTab();
+
+ /**
+ * Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list.
+ *
+ * @param tab Tab to add
+ */
+ public abstract void addTab(Tab tab);
+
+ /**
+ * Insert a tab for use in tabbed navigation mode. The tab will be inserted at
+ * <code>position</code>.
+ *
+ * @param tab The tab to add
+ * @param position The new position of the tab
+ */
+ public abstract void insertTab(Tab tab, int position);
+
+ /**
+ * Remove a tab from the action bar.
+ *
+ * @param tab The tab to remove
+ */
+ public abstract void removeTab(Tab tab);
+
+ /**
+ * Remove a tab from the action bar.
+ *
+ * @param position Position of the tab to remove
+ */
+ public abstract void removeTabAt(int position);
+
+ /**
+ * Select the specified tab. If it is not a child of this action bar it will be added.
+ *
+ * @param tab Tab to select
+ */
+ public abstract void selectTab(Tab tab);
+
+ /**
+ * Select the tab at <code>position</code>
+ *
+ * @param position Position of the tab to select
+ */
+ public abstract void selectTabAt(int position);
+
+ /**
+ * Represents a contextual mode of the Action Bar. Context modes can be used for
+ * modal interactions with activity content and replace the normal Action Bar until finished.
+ * Examples of good contextual modes include selection modes, search, content editing, etc.
+ */
+ public static abstract class ContextMode {
+ /**
+ * Set the title of the context mode. This method will have no visible effect if
+ * a custom view has been set.
+ *
+ * @param title Title string to set
+ *
+ * @see #setCustomView(View)
+ */
+ public abstract void setTitle(CharSequence title);
+
+ /**
+ * Set the subtitle of the context mode. This method will have no visible effect if
+ * a custom view has been set.
+ *
+ * @param subtitle Subtitle string to set
+ *
+ * @see #setCustomView(View)
+ */
+ public abstract void setSubtitle(CharSequence subtitle);
+
+ /**
+ * Set a custom view for this context mode. The custom view will take the place of
+ * the title and subtitle. Useful for things like search boxes.
+ *
+ * @param view Custom view to use in place of the title/subtitle.
+ *
+ * @see #setTitle(CharSequence)
+ * @see #setSubtitle(CharSequence)
+ */
+ public abstract void setCustomView(View view);
+
+ /**
+ * Invalidate the context mode and refresh menu content. The context mode's
+ * {@link ContextModeCallback} will have its
+ * {@link ContextModeCallback#onPrepareContextMode(ContextMode, Menu)} method called.
+ * If it returns true the menu will be scanned for updated content and any relevant changes
+ * will be reflected to the user.
+ */
+ public abstract void invalidate();
+
+ /**
+ * Finish and close this context mode. The context mode's {@link ContextModeCallback} will
+ * have its {@link ContextModeCallback#onDestroyContextMode(ContextMode)} method called.
+ */
+ public abstract void finish();
+
+ /**
+ * Returns the menu of actions that this context mode presents.
+ * @return The context mode's menu.
+ */
+ public abstract Menu getMenu();
+
+ /**
+ * Returns the current title of this context mode.
+ * @return Title text
+ */
+ public abstract CharSequence getTitle();
+
+ /**
+ * Returns the current subtitle of this context mode.
+ * @return Subtitle text
+ */
+ public abstract CharSequence getSubtitle();
+
+ /**
+ * Returns the current custom view for this context mode.
+ * @return The current custom view
+ */
+ public abstract View getCustomView();
+ }
+
+ /**
+ * Callback interface for ActionBar context modes. Supplied to
+ * {@link ActionBar#startContextMode(ContextModeCallback)}, a ContextModeCallback
+ * configures and handles events raised by a user's interaction with a context mode.
+ *
+ * <p>A context mode's lifecycle is as follows:
+ * <ul>
+ * <li>{@link ContextModeCallback#onCreateContextMode(ContextMode, Menu)} once on initial
+ * creation</li>
+ * <li>{@link ContextModeCallback#onPrepareContextMode(ContextMode, Menu)} after creation
+ * and any time the {@link ContextMode} is invalidated</li>
+ * <li>{@link ContextModeCallback#onContextItemClicked(ContextMode, MenuItem)} any time a
+ * contextual action button is clicked</li>
+ * <li>{@link ContextModeCallback#onDestroyContextMode(ContextMode)} when the context mode
+ * is closed</li>
+ * </ul>
+ */
+ public interface ContextModeCallback {
+ /**
+ * Called when a context mode is first created. The menu supplied will be used to generate
+ * action buttons for the context mode.
+ *
+ * @param mode ContextMode being created
+ * @param menu Menu used to populate contextual action buttons
+ * @return true if the context mode should be created, false if entering this context mode
+ * should be aborted.
+ */
+ public boolean onCreateContextMode(ContextMode mode, Menu menu);
+
+ /**
+ * Called to refresh a context mode's action menu whenever it is invalidated.
+ *
+ * @param mode ContextMode being prepared
+ * @param menu Menu used to populate contextual action buttons
+ * @return true if the menu or context mode was updated, false otherwise.
+ */
+ public boolean onPrepareContextMode(ContextMode mode, Menu menu);
+
+ /**
+ * Called to report a user click on a contextual action button.
+ *
+ * @param mode The current ContextMode
+ * @param item The item that was clicked
+ * @return true if this callback handled the event, false if the standard MenuItem
+ * invocation should continue.
+ */
+ public boolean onContextItemClicked(ContextMode mode, MenuItem item);
+
+ /**
+ * Called when a context mode is about to be exited and destroyed.
+ *
+ * @param mode The current ContextMode being destroyed
+ */
+ public void onDestroyContextMode(ContextMode mode);
+ }
+
+ /**
+ * Callback interface for ActionBar navigation events.
+ */
+ public interface NavigationCallback {
+ /**
+ * This method is called whenever a navigation item in your action bar
+ * is selected.
+ *
+ * @param itemPosition Position of the item clicked.
+ * @param itemId ID of the item clicked.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onNavigationItemSelected(int itemPosition, long itemId);
+ }
+
+ /**
+ * A tab in the action bar.
+ *
+ * <p>Tabs manage the hiding and showing of {@link Fragment}s.
+ */
+ public static abstract class Tab {
+ /**
+ * An invalid position for a tab.
+ *
+ * @see #getPosition()
+ */
+ public static final int INVALID_POSITION = -1;
+
+ /**
+ * Return the current position of this tab in the action bar.
+ *
+ * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
+ * the action bar.
+ */
+ public abstract int getPosition();
+
+ /**
+ * Return the icon associated with this tab.
+ *
+ * @return The tab's icon
+ */
+ public abstract Drawable getIcon();
+
+ /**
+ * Return the text of this tab.
+ *
+ * @return The tab's text
+ */
+ public abstract CharSequence getText();
+
+ /**
+ * Set the icon displayed on this tab.
+ *
+ * @param icon The drawable to use as an icon
+ */
+ public abstract void setIcon(Drawable icon);
+
+ /**
+ * Set the text displayed on this tab. Text may be truncated if there is not
+ * room to display the entire string.
+ *
+ * @param text The text to display
+ */
+ public abstract void setText(CharSequence text);
+
+ /**
+ * Returns the fragment that will be shown when this tab is selected.
+ *
+ * @return Fragment associated with this tab
+ */
+ public abstract Fragment getFragment();
+
+ /**
+ * Set the fragment that will be shown when this tab is selected.
+ *
+ * @param fragment Fragment to associate with this tab
+ */
+ public abstract void setFragment(Fragment fragment);
+
+ /**
+ * Select this tab. Only valid if the tab has been added to the action bar.
+ */
+ public abstract void select();
+ }
+}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index f7ccc12..91e4cd5 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -16,19 +16,21 @@
package android.app;
-import com.android.internal.policy.PolicyManager;
+import java.util.ArrayList;
+import java.util.HashMap;
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.Intent;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -39,6 +41,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.text.Selection;
import android.text.SpannableStringBuilder;
@@ -51,6 +54,7 @@ import android.util.Log;
import android.util.SparseArray;
import android.view.ContextMenu;
import android.view.ContextThemeWrapper;
+import android.view.InflateException;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -70,8 +74,9 @@ import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
-import java.util.ArrayList;
-import java.util.HashMap;
+import com.android.internal.app.ActionBarImpl;
+import com.android.internal.policy.PolicyManager;
+import com.android.internal.widget.ActionBarView;
/**
* An activity is a single, focused thing that the user can do. Almost all
@@ -607,6 +612,7 @@ public class Activity extends ContextThemeWrapper
private static long sInstanceCount = 0;
private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";
+ private static final String FRAGMENTS_TAG = "android:fragments";
private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds";
private static final String SAVED_DIALOGS_TAG = "android:savedDialogs";
private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_";
@@ -628,18 +634,27 @@ public class Activity extends ContextThemeWrapper
private ComponentName mComponent;
/*package*/ ActivityInfo mActivityInfo;
/*package*/ ActivityThread mMainThread;
- /*package*/ Object mLastNonConfigurationInstance;
- /*package*/ HashMap<String,Object> mLastNonConfigurationChildInstances;
Activity mParent;
boolean mCalled;
+ boolean mStarted;
private boolean mResumed;
private boolean mStopped;
boolean mFinished;
boolean mStartedActivity;
+ /** true if the activity is being destroyed in order to recreate it with a new configuration */
+ /*package*/ boolean mChangingConfigurations = false;
/*package*/ int mConfigChangeFlags;
/*package*/ Configuration mCurrentConfig;
private SearchManager mSearchManager;
+ static final class NonConfigurationInstances {
+ Object activity;
+ HashMap<String, Object> children;
+ ArrayList<Fragment> fragments;
+ SparseArray<LoaderManager> loaders;
+ }
+ /* package */ NonConfigurationInstances mLastNonConfigurationInstances;
+
private Window mWindow;
private WindowManager mWindowManager;
@@ -647,10 +662,16 @@ public class Activity extends ContextThemeWrapper
/*package*/ boolean mWindowAdded = false;
/*package*/ boolean mVisibleFromServer = false;
/*package*/ boolean mVisibleFromClient = true;
+ /*package*/ ActionBar mActionBar = null;
private CharSequence mTitle;
private int mTitleColor = 0;
+ final FragmentManager mFragments = new FragmentManager();
+
+ SparseArray<LoaderManager> mAllLoaderManagers;
+ LoaderManager mLoaderManager;
+
private static final class ManagedCursor {
ManagedCursor(Cursor cursor) {
mCursor = cursor;
@@ -677,7 +698,7 @@ public class Activity extends ContextThemeWrapper
protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused};
private Thread mUiThread;
- private final Handler mHandler = new Handler();
+ final Handler mHandler = new Handler();
// Used for debug only
/*
@@ -748,6 +769,29 @@ public class Activity extends ContextThemeWrapper
}
/**
+ * Return the LoaderManager for this fragment, creating it if needed.
+ */
+ public LoaderManager getLoaderManager() {
+ if (mLoaderManager != null) {
+ return mLoaderManager;
+ }
+ mLoaderManager = getLoaderManager(-1, false);
+ return mLoaderManager;
+ }
+
+ LoaderManager getLoaderManager(int index, boolean started) {
+ if (mAllLoaderManagers == null) {
+ mAllLoaderManagers = new SparseArray<LoaderManager>();
+ }
+ LoaderManager lm = mAllLoaderManagers.get(index);
+ if (lm == null) {
+ lm = new LoaderManager(started);
+ mAllLoaderManagers.put(index, lm);
+ }
+ return lm;
+ }
+
+ /**
* Calls {@link android.view.Window#getCurrentFocus} on the
* Window of this Activity to return the currently focused view.
*
@@ -801,6 +845,15 @@ public class Activity extends ContextThemeWrapper
protected void onCreate(Bundle savedInstanceState) {
mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowNoDisplay, false);
+ if (mLastNonConfigurationInstances != null) {
+ mAllLoaderManagers = mLastNonConfigurationInstances.loaders;
+ }
+ if (savedInstanceState != null) {
+ Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
+ mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
+ ? mLastNonConfigurationInstances.fragments : null);
+ }
+ mFragments.dispatchCreate();
mCalled = true;
}
@@ -915,6 +968,10 @@ public class Activity extends ContextThemeWrapper
mTitleReady = true;
onTitleChanged(getTitle(), getTitleColor());
}
+ if (mWindow != null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
+ // Invalidate the action bar menu so that it can initialize properly.
+ mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
+ }
mCalled = true;
}
@@ -933,6 +990,10 @@ public class Activity extends ContextThemeWrapper
*/
protected void onStart() {
mCalled = true;
+ mStarted = true;
+ if (mLoaderManager != null) {
+ mLoaderManager.doStart();
+ }
}
/**
@@ -1085,6 +1146,10 @@ public class Activity extends ContextThemeWrapper
*/
protected void onSaveInstanceState(Bundle outState) {
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
+ Parcelable p = mFragments.saveAllState();
+ if (p != null) {
+ outState.putParcelable(FRAGMENTS_TAG, p);
+ }
}
/**
@@ -1407,7 +1472,8 @@ public class Activity extends ContextThemeWrapper
* {@link #onRetainNonConfigurationInstance()}.
*/
public Object getLastNonConfigurationInstance() {
- return mLastNonConfigurationInstance;
+ return mLastNonConfigurationInstances != null
+ ? mLastNonConfigurationInstances.activity : null;
}
/**
@@ -1463,8 +1529,9 @@ public class Activity extends ContextThemeWrapper
* @return Returns the object previously returned by
* {@link #onRetainNonConfigurationChildInstances()}
*/
- HashMap<String,Object> getLastNonConfigurationChildInstances() {
- return mLastNonConfigurationChildInstances;
+ HashMap<String, Object> getLastNonConfigurationChildInstances() {
+ return mLastNonConfigurationInstances != null
+ ? mLastNonConfigurationInstances.children : null;
}
/**
@@ -1478,11 +1545,62 @@ public class Activity extends ContextThemeWrapper
return null;
}
+ NonConfigurationInstances retainNonConfigurationInstances() {
+ Object activity = onRetainNonConfigurationInstance();
+ HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
+ ArrayList<Fragment> fragments = mFragments.retainNonConfig();
+ boolean retainLoaders = false;
+ if (mAllLoaderManagers != null) {
+ // prune out any loader managers that were already stopped, so
+ // have nothing useful to retain.
+ for (int i=mAllLoaderManagers.size()-1; i>=0; i--) {
+ LoaderManager lm = mAllLoaderManagers.valueAt(i);
+ if (lm.mRetaining) {
+ retainLoaders = true;
+ } else {
+ mAllLoaderManagers.removeAt(i);
+ }
+ }
+ }
+ if (activity == null && children == null && fragments == null && !retainLoaders) {
+ return null;
+ }
+
+ NonConfigurationInstances nci = new NonConfigurationInstances();
+ nci.activity = activity;
+ nci.children = children;
+ nci.fragments = fragments;
+ nci.loaders = mAllLoaderManagers;
+ return nci;
+ }
+
public void onLowMemory() {
mCalled = true;
}
/**
+ * Start a series of edit operations on the Fragments associated with
+ * this activity.
+ */
+ public FragmentTransaction openFragmentTransaction() {
+ return new BackStackEntry(mFragments);
+ }
+
+ void invalidateFragmentIndex(int index) {
+ if (mAllLoaderManagers != null) {
+ mAllLoaderManagers.remove(index);
+ }
+ }
+
+ /**
+ * Called when a Fragment is being attached to this activity, immediately
+ * after the call to its {@link Fragment#onAttach Fragment.onAttach()}
+ * method and before {@link Fragment#onCreate Fragment.onCreate()}.
+ */
+ public void onAttachFragment(Fragment fragment) {
+ }
+
+ /**
* Wrapper around
* {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)}
* that gives the resulting {@link Cursor} to call
@@ -1544,40 +1662,6 @@ public class Activity extends ContextThemeWrapper
}
/**
- * Wrapper around {@link Cursor#commitUpdates()} that takes care of noting
- * that the Cursor needs to be requeried. You can call this method in
- * {@link #onPause} or {@link #onStop} to have the system call
- * {@link Cursor#requery} for you if the activity is later resumed. This
- * allows you to avoid determing when to do the requery yourself (which is
- * required for the Cursor to see any data changes that were committed with
- * it).
- *
- * @param c The Cursor whose changes are to be committed.
- *
- * @see #managedQuery(android.net.Uri , String[], String, String[], String)
- * @see #startManagingCursor
- * @see Cursor#commitUpdates()
- * @see Cursor#requery
- * @hide
- */
- @Deprecated
- public void managedCommitUpdates(Cursor c) {
- synchronized (mManagedCursors) {
- final int N = mManagedCursors.size();
- for (int i=0; i<N; i++) {
- ManagedCursor mc = mManagedCursors.get(i);
- if (mc.mCursor == c) {
- c.commitUpdates();
- mc.mUpdated = true;
- return;
- }
- }
- throw new RuntimeException(
- "Cursor " + c + " is not currently managed");
- }
- }
-
- /**
* This method allows the activity to take care of managing the given
* {@link Cursor}'s lifecycle for you based on the activity's lifecycle.
* That is, when the activity is stopped it will automatically call
@@ -1655,7 +1739,52 @@ public class Activity extends ContextThemeWrapper
public View findViewById(int id) {
return getWindow().findViewById(id);
}
-
+
+ /**
+ * Retrieve a reference to this activity's ActionBar.
+ *
+ * <p><em>Note:</em> The ActionBar is initialized when a content view
+ * is set. This function will return null if called before {@link #setContentView}
+ * or {@link #addContentView}.
+ * @return The Activity's ActionBar, or null if it does not have one.
+ */
+ public ActionBar getActionBar() {
+ return mActionBar;
+ }
+
+ /**
+ * Creates a new ActionBar, locates the inflated ActionBarView,
+ * initializes the ActionBar with the view, and sets mActionBar.
+ */
+ private void initActionBar() {
+ Window window = getWindow();
+ if (!window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
+ return;
+ }
+
+ mActionBar = new ActionBarImpl(this);
+ }
+
+ /**
+ * Finds a fragment that was identified by the given id either when inflated
+ * from XML or as the container ID when added in a transaction. This only
+ * returns fragments that are currently added to the activity's content.
+ * @return The fragment if found or null otherwise.
+ */
+ public Fragment findFragmentById(int id) {
+ return mFragments.findFragmentById(id);
+ }
+
+ /**
+ * Finds a fragment that was identified by the given tag either when inflated
+ * from XML or as supplied when added in a transaction. This only
+ * returns fragments that are currently added to the activity's content.
+ * @return The fragment if found or null otherwise.
+ */
+ public Fragment findFragmentByTag(String tag) {
+ return mFragments.findFragmentByTag(tag);
+ }
+
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
@@ -1664,6 +1793,7 @@ public class Activity extends ContextThemeWrapper
*/
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
+ initActionBar();
}
/**
@@ -1675,6 +1805,7 @@ public class Activity extends ContextThemeWrapper
*/
public void setContentView(View view) {
getWindow().setContentView(view);
+ initActionBar();
}
/**
@@ -1687,6 +1818,7 @@ public class Activity extends ContextThemeWrapper
*/
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
+ initActionBar();
}
/**
@@ -1698,6 +1830,7 @@ public class Activity extends ContextThemeWrapper
*/
public void addContentView(View view, ViewGroup.LayoutParams params) {
getWindow().addContentView(view, params);
+ initActionBar();
}
/**
@@ -1921,12 +2054,25 @@ public class Activity extends ContextThemeWrapper
}
/**
+ * Pop the last fragment transition from the local activity's fragment
+ * back stack. If there is nothing to pop, false is returned.
+ * @param name If non-null, this is the name of a previous back state
+ * to look for; if found, all states up to (but not including) that
+ * state will be popped. If null, only the top state is popped.
+ */
+ public boolean popBackStack(String name) {
+ return mFragments.popBackStackState(mHandler, name);
+ }
+
+ /**
* Called when the activity has detected the user's press of the back
* key. The default implementation simply finishes the current activity,
* but you can override this to do whatever you want.
*/
public void onBackPressed() {
- finish();
+ if (!popBackStack(null)) {
+ finish();
+ }
}
/**
@@ -2164,7 +2310,9 @@ public class Activity extends ContextThemeWrapper
*/
public boolean onCreatePanelMenu(int featureId, Menu menu) {
if (featureId == Window.FEATURE_OPTIONS_PANEL) {
- return onCreateOptionsMenu(menu);
+ boolean show = onCreateOptionsMenu(menu);
+ show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater());
+ return show;
}
return false;
}
@@ -2181,6 +2329,7 @@ public class Activity extends ContextThemeWrapper
public boolean onPreparePanel(int featureId, View view, Menu menu) {
if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) {
boolean goforit = onPrepareOptionsMenu(menu);
+ goforit |= mFragments.dispatchPrepareOptionsMenu(menu);
return goforit && menu.hasVisibleItems();
}
return true;
@@ -2211,11 +2360,17 @@ public class Activity extends ContextThemeWrapper
// doesn't call through to superclass's implmeentation of each
// of these methods below
EventLog.writeEvent(50000, 0, item.getTitleCondensed());
- return onOptionsItemSelected(item);
+ if (onOptionsItemSelected(item)) {
+ return true;
+ }
+ return mFragments.dispatchOptionsItemSelected(item);
case Window.FEATURE_CONTEXT_MENU:
EventLog.writeEvent(50000, 1, item.getTitleCondensed());
- return onContextItemSelected(item);
+ if (onContextItemSelected(item)) {
+ return true;
+ }
+ return mFragments.dispatchContextItemSelected(item);
default:
return false;
@@ -2234,6 +2389,7 @@ public class Activity extends ContextThemeWrapper
public void onPanelClosed(int featureId, Menu menu) {
switch (featureId) {
case Window.FEATURE_OPTIONS_PANEL:
+ mFragments.dispatchOptionsMenuClosed(menu);
onOptionsMenuClosed(menu);
break;
@@ -2244,6 +2400,15 @@ public class Activity extends ContextThemeWrapper
}
/**
+ * Declare that the options menu has changed, so should be recreated.
+ * The {@link #onCreateOptionsMenu(Menu)} method will be called the next
+ * time it needs to be displayed.
+ */
+ public void invalidateOptionsMenu() {
+ mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL);
+ }
+
+ /**
* Initialize the contents of the Activity's standard options menu. You
* should place your menu items in to <var>menu</var>.
*
@@ -3085,6 +3250,36 @@ public class Activity extends ContextThemeWrapper
}
/**
+ * This is called when a Fragment in this activity calls its
+ * {@link Fragment#startActivity} or {@link Fragment#startActivityForResult}
+ * method.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param fragment The fragment making the call.
+ * @param intent The intent to start.
+ * @param requestCode Reply request code. < 0 if reply is not requested.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Fragment#startActivity
+ * @see Fragment#startActivityForResult
+ */
+ public void startActivityFromFragment(Fragment fragment, Intent intent,
+ int requestCode) {
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, fragment,
+ intent, requestCode);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, fragment.mWho, requestCode,
+ ar.getResultCode(), ar.getResultData());
+ }
+ }
+
+ /**
* Like {@link #startActivityFromChild(Activity, Intent, int)}, but
* taking a IntentSender; see
* {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}
@@ -3243,6 +3438,19 @@ public class Activity extends ContextThemeWrapper
}
/**
+ * Check to see whether this activity is in the process of being destroyed in order to be
+ * recreated with a new configuration. This is often used in
+ * {@link #onStop} to determine whether the state needs to be cleaned up or will be passed
+ * on to the next instance of the activity via {@link #onRetainNonConfigurationInstance()}.
+ *
+ * @return If the activity is being torn down in order to be recreated with a new configuration,
+ * returns true; else returns false.
+ */
+ public boolean isChangingConfigurations() {
+ return mChangingConfigurations;
+ }
+
+ /**
* Call this when your activity is done and should be closed. The
* ActivityResult is propagated back to whoever launched you via
* onActivityResult().
@@ -3343,8 +3551,7 @@ public class Activity extends ContextThemeWrapper
* @see #createPendingResult
* @see #setResult(int)
*/
- protected void onActivityResult(int requestCode, int resultCode,
- Intent data) {
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
/**
@@ -3728,15 +3935,69 @@ public class Activity extends ContextThemeWrapper
}
/**
- * Stub implementation of {@link android.view.LayoutInflater.Factory#onCreateView} used when
- * inflating with the LayoutInflater returned by {@link #getSystemService}. This
- * implementation simply returns null for all view names.
+ * Standard implementation of
+ * {@link android.view.LayoutInflater.Factory#onCreateView} used when
+ * inflating with the LayoutInflater returned by {@link #getSystemService}.
+ * This implementation handles <fragment> tags to embed fragments inside
+ * of the activity.
*
* @see android.view.LayoutInflater#createView
* @see android.view.Window#getLayoutInflater
*/
public View onCreateView(String name, Context context, AttributeSet attrs) {
- return null;
+ if (!"fragment".equals(name)) {
+ return null;
+ }
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Fragment);
+ String fname = a.getString(com.android.internal.R.styleable.Fragment_name);
+ int id = a.getResourceId(com.android.internal.R.styleable.Fragment_id, 0);
+ String tag = a.getString(com.android.internal.R.styleable.Fragment_tag);
+ a.recycle();
+
+ if (id == 0) {
+ throw new IllegalArgumentException(attrs.getPositionDescription()
+ + ": Must specify unique android:id for " + fname);
+ }
+
+ try {
+ // If we restored from a previous state, we may already have
+ // instantiated this fragment from the state and should use
+ // that instance instead of making a new one.
+ Fragment fragment = mFragments.findFragmentById(id);
+ if (FragmentManager.DEBUG) Log.v(TAG, "onCreateView: id=0x"
+ + Integer.toHexString(id) + " fname=" + fname
+ + " existing=" + fragment);
+ if (fragment == null) {
+ fragment = Fragment.instantiate(this, fname);
+ fragment.mFromLayout = true;
+ fragment.mFragmentId = id;
+ fragment.mTag = tag;
+ fragment.mImmediateActivity = this;
+ mFragments.addFragment(fragment, true);
+ }
+ // If this fragment is newly instantiated (either right now, or
+ // from last saved state), then give it the attributes to
+ // initialize itself.
+ if (!fragment.mRetaining) {
+ fragment.onInflate(this, attrs, fragment.mSavedFragmentState);
+ }
+ if (fragment.mView == null) {
+ throw new IllegalStateException("Fragment " + fname
+ + " did not create a view.");
+ }
+ fragment.mView.setId(id);
+ if (fragment.mView.getTag() == null) {
+ fragment.mView.setTag(tag);
+ }
+ return fragment.mView;
+ } catch (Exception e) {
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Error inflating fragment " + fname);
+ ie.initCause(e);
+ throw ie;
+ }
}
/**
@@ -3787,23 +4048,25 @@ public class Activity extends ContextThemeWrapper
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token,
Application application, Intent intent, ActivityInfo info, CharSequence title,
- Activity parent, String id, Object lastNonConfigurationInstance,
+ Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances,
Configuration config) {
attach(context, aThread, instr, token, 0, application, intent, info, title, parent, id,
- lastNonConfigurationInstance, null, config);
+ lastNonConfigurationInstances, config);
}
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
- Object lastNonConfigurationInstance,
- HashMap<String,Object> lastNonConfigurationChildInstances,
+ NonConfigurationInstances lastNonConfigurationInstances,
Configuration config) {
attachBaseContext(context);
+ mFragments.attachActivity(this);
+
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
+ mWindow.getLayoutInflater().setFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
@@ -3820,8 +4083,7 @@ public class Activity extends ContextThemeWrapper
mTitle = title;
mParent = parent;
mEmbeddedID = id;
- mLastNonConfigurationInstance = lastNonConfigurationInstance;
- mLastNonConfigurationChildInstances = lastNonConfigurationChildInstances;
+ mLastNonConfigurationInstances = lastNonConfigurationInstances;
mWindow.setWindowManager(null, mToken, mComponent.flattenToString());
if (mParent != null) {
@@ -3835,14 +4097,26 @@ public class Activity extends ContextThemeWrapper
return mParent != null ? mParent.getActivityToken() : mToken;
}
+ final void performCreate(Bundle icicle) {
+ onCreate(icicle);
+ mFragments.dispatchActivityCreated();
+ }
+
final void performStart() {
mCalled = false;
+ mFragments.execPendingActions();
mInstrumentation.callActivityOnStart(this);
if (!mCalled) {
throw new SuperNotCalledException(
"Activity " + mComponent.toShortString() +
" did not call through to super.onStart()");
}
+ mFragments.dispatchStart();
+ if (mAllLoaderManagers != null) {
+ for (int i=mAllLoaderManagers.size()-1; i>=0; i--) {
+ mAllLoaderManagers.valueAt(i).finishRetain();
+ }
+ }
}
final void performRestart() {
@@ -3874,7 +4148,9 @@ public class Activity extends ContextThemeWrapper
final void performResume() {
performRestart();
- mLastNonConfigurationInstance = null;
+ mFragments.execPendingActions();
+
+ mLastNonConfigurationInstances = null;
// First call onResume() -before- setting mResumed, so we don't
// send out any status bar / menu notifications the client makes.
@@ -3889,6 +4165,10 @@ public class Activity extends ContextThemeWrapper
// Now really resume, and install the current status bar and menu.
mResumed = true;
mCalled = false;
+
+ mFragments.dispatchResume();
+ mFragments.execPendingActions();
+
onPostResume();
if (!mCalled) {
throw new SuperNotCalledException(
@@ -3898,6 +4178,7 @@ public class Activity extends ContextThemeWrapper
}
final void performPause() {
+ mFragments.dispatchPause();
onPause();
}
@@ -3907,11 +4188,24 @@ public class Activity extends ContextThemeWrapper
}
final void performStop() {
+ if (mStarted) {
+ mStarted = false;
+ if (mLoaderManager != null) {
+ if (!mChangingConfigurations) {
+ mLoaderManager.doStop();
+ } else {
+ mLoaderManager.doRetain();
+ }
+ }
+ }
+
if (!mStopped) {
if (mWindow != null) {
mWindow.closeAllPanels();
}
+ mFragments.dispatchStop();
+
mCalled = false;
mInstrumentation.callActivityOnStop(this);
if (!mCalled) {
@@ -3936,6 +4230,11 @@ public class Activity extends ContextThemeWrapper
mResumed = false;
}
+ final void performDestroy() {
+ mFragments.dispatchDestroy();
+ onDestroy();
+ }
+
final boolean isResumed() {
return mResumed;
}
@@ -3947,6 +4246,11 @@ public class Activity extends ContextThemeWrapper
+ ", resCode=" + resultCode + ", data=" + data);
if (who == null) {
onActivityResult(requestCode, resultCode, data);
+ } else {
+ Fragment frag = mFragments.findFragmentByWho(who);
+ if (frag != null) {
+ frag.onActivityResult(requestCode, resultCode, data);
+ }
}
}
}
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 1fe85e6..43a08b5 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -1294,6 +1294,19 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM
return true;
}
+ case DUMP_HEAP_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ String process = data.readString();
+ boolean managed = data.readInt() != 0;
+ String path = data.readString();
+ ParcelFileDescriptor fd = data.readInt() != 0
+ ? data.readFileDescriptor() : null;
+ boolean res = dumpHeap(process, managed, path, fd);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
}
return super.onTransact(code, data, reply, flags);
@@ -2874,6 +2887,28 @@ class ActivityManagerProxy implements IActivityManager
data.recycle();
reply.recycle();
}
-
+
+ public boolean dumpHeap(String process, boolean managed,
+ String path, ParcelFileDescriptor fd) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeString(process);
+ data.writeInt(managed ? 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(DUMP_HEAP_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+
private IBinder mRemote;
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 0fb2b49..c800fbe 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -30,6 +30,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.InstrumentationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
@@ -195,8 +196,7 @@ public final class ActivityThread {
Window window;
Activity parent;
String embeddedID;
- Object lastNonConfigurationInstance;
- HashMap<String,Object> lastNonConfigurationChildInstances;
+ Activity.NonConfigurationInstances lastNonConfigurationInstances;
boolean paused;
boolean stopped;
boolean hideForNow;
@@ -357,11 +357,16 @@ public final class ActivityThread {
ParcelFileDescriptor fd;
}
+ private static final class DumpHeapData {
+ 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";
private static final String TWO_COUNT_COLUMNS = "%17s %8d %17s %8d";
- private static final String DB_INFO_FORMAT = " %8d %8d %10d %s";
+ private static final String DB_INFO_FORMAT = " %4d %6d %8d %14s %s";
// Formatting for checkin service - update version if row format changes
private static final int ACTIVITY_THREAD_CHECKIN_VERSION = 1;
@@ -624,6 +629,13 @@ public final class ActivityThread {
queueOrSendMessage(H.PROFILER_CONTROL, pcd, start ? 1 : 0);
}
+ public void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd) {
+ DumpHeapData dhd = new DumpHeapData();
+ dhd.path = path;
+ dhd.fd = fd;
+ queueOrSendMessage(H.DUMP_HEAP, dhd, managed ? 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
@@ -761,7 +773,7 @@ public final class ActivityThread {
for (int i = 0; i < stats.dbStats.size(); i++) {
DbStats dbStats = stats.dbStats.get(i);
printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize,
- dbStats.lookaside, dbStats.dbName);
+ dbStats.lookaside, dbStats.cache, dbStats.dbName);
pw.print(',');
}
@@ -812,11 +824,12 @@ public final class ActivityThread {
int N = stats.dbStats.size();
if (N > 0) {
pw.println(" DATABASES");
- printRow(pw, " %8s %8s %10s %s", "Pagesize", "Dbsize", "Lookaside", "Dbname");
+ printRow(pw, " %4s %6s %8s %14s %s", "pgsz", "dbsz", "lkaside", "cache",
+ "Dbname");
for (int i = 0; i < N; i++) {
DbStats dbStats = stats.dbStats.get(i);
printRow(pw, DB_INFO_FORMAT, dbStats.pageSize, dbStats.dbSize,
- dbStats.lookaside, dbStats.dbName);
+ dbStats.lookaside, dbStats.cache, dbStats.dbName);
}
}
@@ -874,6 +887,7 @@ public final class ActivityThread {
public static final int ENABLE_JIT = 132;
public static final int DISPATCH_PACKAGE_BROADCAST = 133;
public static final int SCHEDULE_CRASH = 134;
+ public static final int DUMP_HEAP = 135;
String codeToString(int code) {
if (localLOGV) {
switch (code) {
@@ -912,6 +926,7 @@ public final class ActivityThread {
case ENABLE_JIT: return "ENABLE_JIT";
case DISPATCH_PACKAGE_BROADCAST: return "DISPATCH_PACKAGE_BROADCAST";
case SCHEDULE_CRASH: return "SCHEDULE_CRASH";
+ case DUMP_HEAP: return "DUMP_HEAP";
}
}
return "(unknown)";
@@ -1037,13 +1052,35 @@ public final class ActivityThread {
break;
case SCHEDULE_CRASH:
throw new RemoteServiceException((String)msg.obj);
+ case DUMP_HEAP:
+ handleDumpHeap(msg.arg1 != 0, (DumpHeapData)msg.obj);
+ break;
}
}
void maybeSnapshot() {
if (mBoundApplication != null) {
- SamplingProfilerIntegration.writeSnapshot(
- mBoundApplication.processName);
+ // convert the *private* ActivityThread.PackageInfo to *public* known
+ // android.content.pm.PackageInfo
+ String packageName = mBoundApplication.info.mPackageName;
+ android.content.pm.PackageInfo packageInfo = null;
+ try {
+ Context context = getSystemContext();
+ if(context == null) {
+ Log.e(TAG, "cannot get a valid context");
+ return;
+ }
+ PackageManager pm = context.getPackageManager();
+ if(pm == null) {
+ Log.e(TAG, "cannot get a valid PackageManager");
+ return;
+ }
+ packageInfo = pm.getPackageInfo(
+ packageName, PackageManager.GET_ACTIVITIES);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "cannot get package info for " + packageName, e);
+ }
+ SamplingProfilerIntegration.writeSnapshot(mBoundApplication.processName, packageInfo);
}
}
}
@@ -1434,7 +1471,7 @@ public final class ActivityThread {
public final Activity startActivityNow(Activity parent, String id,
Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,
- Object lastNonConfigurationInstance) {
+ Activity.NonConfigurationInstances lastNonConfigurationInstances) {
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident = 0;
@@ -1443,7 +1480,7 @@ public final class ActivityThread {
r.parent = parent;
r.embeddedID = id;
r.activityInfo = activityInfo;
- r.lastNonConfigurationInstance = lastNonConfigurationInstance;
+ r.lastNonConfigurationInstances = lastNonConfigurationInstances;
if (localLOGV) {
ComponentName compname = intent.getComponent();
String name;
@@ -1565,14 +1602,12 @@ public final class ActivityThread {
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
- r.embeddedID, r.lastNonConfigurationInstance,
- r.lastNonConfigurationChildInstances, config);
+ r.embeddedID, r.lastNonConfigurationInstances, config);
if (customIntent != null) {
activity.mIntent = customIntent;
}
- r.lastNonConfigurationInstance = null;
- r.lastNonConfigurationChildInstances = null;
+ r.lastNonConfigurationInstances = null;
activity.mStartedActivity = false;
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
@@ -2541,6 +2576,9 @@ public final class ActivityThread {
if (finishing) {
r.activity.mFinished = true;
}
+ if (getNonConfigInstance) {
+ r.activity.mChangingConfigurations = true;
+ }
if (!r.paused) {
try {
r.activity.mCalled = false;
@@ -2581,8 +2619,8 @@ public final class ActivityThread {
}
if (getNonConfigInstance) {
try {
- r.lastNonConfigurationInstance
- = r.activity.onRetainNonConfigurationInstance();
+ r.lastNonConfigurationInstances
+ = r.activity.retainNonConfigurationInstances();
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
@@ -2591,22 +2629,10 @@ public final class ActivityThread {
+ ": " + e.toString(), e);
}
}
- try {
- r.lastNonConfigurationChildInstances
- = r.activity.onRetainNonConfigurationChildInstances();
- } catch (Exception e) {
- if (!mInstrumentation.onException(r.activity, e)) {
- throw new RuntimeException(
- "Unable to retain child activities "
- + safeToComponentShortString(r.intent)
- + ": " + e.toString(), e);
- }
- }
-
}
try {
r.activity.mCalled = false;
- r.activity.onDestroy();
+ mInstrumentation.callActivityOnDestroy(r.activity);
if (!r.activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + safeToComponentShortString(r.intent) +
@@ -3018,6 +3044,25 @@ public final class ActivityThread {
}
}
+ final void handleDumpHeap(boolean managed, DumpHeapData dhd) {
+ if (managed) {
+ try {
+ Debug.dumpHprofData(dhd.path, dhd.fd.getFileDescriptor());
+ } catch (IOException e) {
+ Slog.w(TAG, "Managed heap dump failed on path " + dhd.path
+ + " -- can the process access this path?");
+ } finally {
+ try {
+ dhd.fd.close();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failure closing profile fd", e);
+ }
+ }
+ } else {
+ Debug.dumpNativeHeap(dhd.fd.getFileDescriptor());
+ }
+ }
+
final void handleDispatchPackageBroadcast(int cmd, String[] packages) {
boolean hasPkgInfo = false;
if (packages != null) {
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
index 1c20062..dc2145f 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -403,6 +403,17 @@ public abstract class ApplicationThreadNative extends Binder
scheduleCrash(msg);
return true;
}
+
+ case DUMP_HEAP_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ boolean managed = data.readInt() != 0;
+ String path = data.readString();
+ ParcelFileDescriptor fd = data.readInt() != 0
+ ? data.readFileDescriptor() : null;
+ dumpHeap(managed, path, fd);
+ return true;
+ }
}
return super.onTransact(code, data, reply, flags);
@@ -829,5 +840,22 @@ class ApplicationThreadProxy implements IApplicationThread {
data.recycle();
}
+
+ public void dumpHeap(boolean managed, String path,
+ ParcelFileDescriptor fd) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeInt(managed ? 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(DUMP_HEAP_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
}
diff --git a/core/java/android/app/BackStackEntry.java b/core/java/android/app/BackStackEntry.java
new file mode 100644
index 0000000..c958e26
--- /dev/null
+++ b/core/java/android/app/BackStackEntry.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+final class BackStackState implements Parcelable {
+ final int[] mOps;
+ final int mTransition;
+ final int mTransitionStyle;
+ final String mName;
+
+ public BackStackState(FragmentManager fm, BackStackEntry bse) {
+ int numRemoved = 0;
+ BackStackEntry.Op op = bse.mHead;
+ while (op != null) {
+ if (op.removed != null) numRemoved += op.removed.size();
+ op = op.next;
+ }
+ mOps = new int[bse.mNumOp*5 + numRemoved];
+
+ op = bse.mHead;
+ int pos = 0;
+ while (op != null) {
+ mOps[pos++] = op.cmd;
+ mOps[pos++] = op.fragment.mIndex;
+ mOps[pos++] = op.enterAnim;
+ mOps[pos++] = op.exitAnim;
+ if (op.removed != null) {
+ final int N = op.removed.size();
+ mOps[pos++] = N;
+ for (int i=0; i<N; i++) {
+ mOps[pos++] = op.removed.get(i).mIndex;
+ }
+ } else {
+ mOps[pos++] = 0;
+ }
+ op = op.next;
+ }
+ mTransition = bse.mTransition;
+ mTransitionStyle = bse.mTransitionStyle;
+ mName = bse.mName;
+ }
+
+ public BackStackState(Parcel in) {
+ mOps = in.createIntArray();
+ mTransition = in.readInt();
+ mTransitionStyle = in.readInt();
+ mName = in.readString();
+ }
+
+ public BackStackEntry instantiate(FragmentManager fm) {
+ BackStackEntry bse = new BackStackEntry(fm);
+ int pos = 0;
+ while (pos < mOps.length) {
+ BackStackEntry.Op op = new BackStackEntry.Op();
+ op.cmd = mOps[pos++];
+ Fragment f = fm.mActive.get(mOps[pos++]);
+ f.mBackStackNesting++;
+ op.fragment = f;
+ op.enterAnim = mOps[pos++];
+ op.exitAnim = mOps[pos++];
+ final int N = mOps[pos++];
+ if (N > 0) {
+ op.removed = new ArrayList<Fragment>(N);
+ for (int i=0; i<N; i++) {
+ op.removed.add(fm.mActive.get(mOps[pos++]));
+ }
+ }
+ bse.addOp(op);
+ }
+ bse.mTransition = mTransition;
+ bse.mTransitionStyle = mTransitionStyle;
+ bse.mName = mName;
+ return bse;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(mOps);
+ dest.writeInt(mTransition);
+ dest.writeInt(mTransitionStyle);
+ dest.writeString(mName);
+ }
+
+ public static final Parcelable.Creator<BackStackState> CREATOR
+ = new Parcelable.Creator<BackStackState>() {
+ public BackStackState createFromParcel(Parcel in) {
+ return new BackStackState(in);
+ }
+
+ public BackStackState[] newArray(int size) {
+ return new BackStackState[size];
+ }
+ };
+}
+
+/**
+ * @hide Entry of an operation on the fragment back stack.
+ */
+final class BackStackEntry implements FragmentTransaction, Runnable {
+ static final String TAG = "BackStackEntry";
+
+ final FragmentManager mManager;
+
+ static final int OP_NULL = 0;
+ static final int OP_ADD = 1;
+ static final int OP_REPLACE = 2;
+ static final int OP_REMOVE = 3;
+ static final int OP_HIDE = 4;
+ static final int OP_SHOW = 5;
+
+ static final class Op {
+ Op next;
+ Op prev;
+ int cmd;
+ Fragment fragment;
+ int enterAnim;
+ int exitAnim;
+ ArrayList<Fragment> removed;
+ }
+
+ Op mHead;
+ Op mTail;
+ int mNumOp;
+ int mEnterAnim;
+ int mExitAnim;
+ int mTransition;
+ int mTransitionStyle;
+ boolean mAddToBackStack;
+ String mName;
+ boolean mCommitted;
+
+ public BackStackEntry(FragmentManager manager) {
+ mManager = manager;
+ }
+
+ void addOp(Op op) {
+ if (mHead == null) {
+ mHead = mTail = op;
+ } else {
+ op.prev = mTail;
+ mTail.next = op;
+ mTail = op;
+ }
+ op.enterAnim = mEnterAnim;
+ op.exitAnim = mExitAnim;
+ mNumOp++;
+ }
+
+ public FragmentTransaction add(Fragment fragment, String tag) {
+ doAddOp(0, fragment, tag, OP_ADD);
+ return this;
+ }
+
+ public FragmentTransaction add(int containerViewId, Fragment fragment) {
+ doAddOp(containerViewId, fragment, null, OP_ADD);
+ return this;
+ }
+
+ public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) {
+ doAddOp(containerViewId, fragment, tag, OP_ADD);
+ return this;
+ }
+
+ private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
+ if (fragment.mImmediateActivity != null) {
+ throw new IllegalStateException("Fragment already added: " + fragment);
+ }
+ fragment.mImmediateActivity = mManager.mActivity;
+
+ if (tag != null) {
+ if (fragment.mTag != null && !tag.equals(fragment.mTag)) {
+ throw new IllegalStateException("Can't change tag of fragment "
+ + fragment + ": was " + fragment.mTag
+ + " now " + tag);
+ }
+ fragment.mTag = tag;
+ }
+
+ if (containerViewId != 0) {
+ if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
+ throw new IllegalStateException("Can't change container ID of fragment "
+ + fragment + ": was " + fragment.mFragmentId
+ + " now " + containerViewId);
+ }
+ fragment.mContainerId = fragment.mFragmentId = containerViewId;
+ }
+
+ Op op = new Op();
+ op.cmd = opcmd;
+ op.fragment = fragment;
+ addOp(op);
+ }
+
+ public FragmentTransaction replace(int containerViewId, Fragment fragment) {
+ return replace(containerViewId, fragment, null);
+ }
+
+ public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
+ if (containerViewId == 0) {
+ throw new IllegalArgumentException("Must use non-zero containerViewId");
+ }
+
+ doAddOp(containerViewId, fragment, tag, OP_REPLACE);
+ return this;
+ }
+
+ public FragmentTransaction remove(Fragment fragment) {
+ if (fragment.mImmediateActivity == null) {
+ throw new IllegalStateException("Fragment not added: " + fragment);
+ }
+ fragment.mImmediateActivity = null;
+
+ Op op = new Op();
+ op.cmd = OP_REMOVE;
+ op.fragment = fragment;
+ addOp(op);
+
+ return this;
+ }
+
+ public FragmentTransaction hide(Fragment fragment) {
+ if (fragment.mImmediateActivity == null) {
+ throw new IllegalStateException("Fragment not added: " + fragment);
+ }
+
+ Op op = new Op();
+ op.cmd = OP_HIDE;
+ op.fragment = fragment;
+ addOp(op);
+
+ return this;
+ }
+
+ public FragmentTransaction show(Fragment fragment) {
+ if (fragment.mImmediateActivity == null) {
+ throw new IllegalStateException("Fragment not added: " + fragment);
+ }
+
+ Op op = new Op();
+ op.cmd = OP_SHOW;
+ op.fragment = fragment;
+ addOp(op);
+
+ return this;
+ }
+
+ public FragmentTransaction setCustomAnimations(int enter, int exit) {
+ mEnterAnim = enter;
+ mExitAnim = exit;
+ return this;
+ }
+
+ public FragmentTransaction setTransition(int transition) {
+ mTransition = transition;
+ return this;
+ }
+
+ public FragmentTransaction setTransitionStyle(int styleRes) {
+ mTransitionStyle = styleRes;
+ return this;
+ }
+
+ public FragmentTransaction addToBackStack(String name) {
+ mAddToBackStack = true;
+ mName = name;
+ return this;
+ }
+
+ public void commit() {
+ if (mCommitted) throw new IllegalStateException("commit already called");
+ if (FragmentManager.DEBUG) Log.v(TAG, "Commit: " + this);
+ mCommitted = true;
+ mManager.enqueueAction(this);
+ }
+
+ public void run() {
+ if (FragmentManager.DEBUG) Log.v(TAG, "Run: " + this);
+
+ Op op = mHead;
+ while (op != null) {
+ switch (op.cmd) {
+ case OP_ADD: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting++;
+ }
+ f.mNextAnim = op.enterAnim;
+ mManager.addFragment(f, false);
+ } break;
+ case OP_REPLACE: {
+ Fragment f = op.fragment;
+ if (mManager.mAdded != null) {
+ for (int i=0; i<mManager.mAdded.size(); i++) {
+ Fragment old = mManager.mAdded.get(i);
+ if (FragmentManager.DEBUG) Log.v(TAG,
+ "OP_REPLACE: adding=" + f + " old=" + old);
+ if (old.mContainerId == f.mContainerId) {
+ if (op.removed == null) {
+ op.removed = new ArrayList<Fragment>();
+ }
+ op.removed.add(old);
+ if (mAddToBackStack) {
+ old.mBackStackNesting++;
+ }
+ old.mNextAnim = op.exitAnim;
+ mManager.removeFragment(old, mTransition, mTransitionStyle);
+ }
+ }
+ }
+ if (mAddToBackStack) {
+ f.mBackStackNesting++;
+ }
+ f.mNextAnim = op.enterAnim;
+ mManager.addFragment(f, false);
+ } break;
+ case OP_REMOVE: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting++;
+ }
+ f.mNextAnim = op.exitAnim;
+ mManager.removeFragment(f, mTransition, mTransitionStyle);
+ } break;
+ case OP_HIDE: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting++;
+ }
+ f.mNextAnim = op.exitAnim;
+ mManager.hideFragment(f, mTransition, mTransitionStyle);
+ } break;
+ case OP_SHOW: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting++;
+ }
+ f.mNextAnim = op.enterAnim;
+ mManager.showFragment(f, mTransition, mTransitionStyle);
+ } break;
+ default: {
+ throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
+ }
+ }
+
+ op = op.next;
+ }
+
+ mManager.moveToState(mManager.mCurState, mTransition,
+ mTransitionStyle, true);
+ if (mManager.mNeedMenuInvalidate && mManager.mActivity != null) {
+ mManager.mActivity.invalidateOptionsMenu();
+ mManager.mNeedMenuInvalidate = false;
+ }
+
+ if (mAddToBackStack) {
+ mManager.addBackStackState(this);
+ }
+ }
+
+ public void popFromBackStack() {
+ if (FragmentManager.DEBUG) Log.v(TAG, "popFromBackStack: " + this);
+
+ Op op = mTail;
+ while (op != null) {
+ switch (op.cmd) {
+ case OP_ADD: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting--;
+ }
+ mManager.removeFragment(f,
+ FragmentManager.reverseTransit(mTransition),
+ mTransitionStyle);
+ } break;
+ case OP_REPLACE: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting--;
+ }
+ mManager.removeFragment(f,
+ FragmentManager.reverseTransit(mTransition),
+ mTransitionStyle);
+ if (op.removed != null) {
+ for (int i=0; i<op.removed.size(); i++) {
+ Fragment old = op.removed.get(i);
+ if (mAddToBackStack) {
+ old.mBackStackNesting--;
+ }
+ mManager.addFragment(old, false);
+ }
+ }
+ } break;
+ case OP_REMOVE: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting--;
+ }
+ mManager.addFragment(f, false);
+ } break;
+ case OP_HIDE: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting--;
+ }
+ mManager.showFragment(f,
+ FragmentManager.reverseTransit(mTransition), mTransitionStyle);
+ } break;
+ case OP_SHOW: {
+ Fragment f = op.fragment;
+ if (mAddToBackStack) {
+ f.mBackStackNesting--;
+ }
+ mManager.hideFragment(f,
+ FragmentManager.reverseTransit(mTransition), mTransitionStyle);
+ } break;
+ default: {
+ throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
+ }
+ }
+
+ op = op.prev;
+ }
+
+ mManager.moveToState(mManager.mCurState,
+ FragmentManager.reverseTransit(mTransition), mTransitionStyle, true);
+ if (mManager.mNeedMenuInvalidate && mManager.mActivity != null) {
+ mManager.mActivity.invalidateOptionsMenu();
+ mManager.mNeedMenuInvalidate = false;
+ }
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getTransition() {
+ return mTransition;
+ }
+
+ public int getTransitionStyle() {
+ return mTransitionStyle;
+ }
+}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 9deaa31..a2a74f8 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -56,6 +56,7 @@ import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
+import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.graphics.Bitmap;
@@ -542,6 +543,15 @@ class ContextImpl extends Context {
}
@Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory,
+ DatabaseErrorHandler errorHandler) {
+ File f = validateFilePath(name, true);
+ SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f.getPath(), factory, errorHandler);
+ setFilePermissionsFromMode(f.getPath(), mode, 0);
+ return db;
+ }
+
+ @Override
public boolean deleteDatabase(String name) {
try {
File f = validateFilePath(name, false);
@@ -619,7 +629,8 @@ class ContextImpl extends Context {
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
- getOuterContext(), mMainThread.getApplicationThread(), null, null, intent, -1);
+ getOuterContext(), mMainThread.getApplicationThread(), null,
+ (Activity)null, intent, -1);
}
@Override
@@ -2757,6 +2768,13 @@ class ContextImpl extends Context {
return v != null ? v : defValue;
}
}
+
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ synchronized (this) {
+ Set<String> v = (Set<String>) mMap.get(key);
+ return v != null ? v : defValues;
+ }
+ }
public int getInt(String key, int defValue) {
synchronized (this) {
@@ -2799,6 +2817,12 @@ class ContextImpl extends Context {
return this;
}
}
+ public Editor putStringSet(String key, Set<String> values) {
+ synchronized (this) {
+ mModified.put(key, values);
+ return this;
+ }
+ }
public Editor putInt(String key, int value) {
synchronized (this) {
mModified.put(key, value);
diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java
new file mode 100644
index 0000000..51cce5e
--- /dev/null
+++ b/core/java/android/app/Fragment.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ComponentCallbacks;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.animation.Animation;
+import android.widget.AdapterView;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+
+final class FragmentState implements Parcelable {
+ static final String VIEW_STATE_TAG = "android:view_state";
+
+ final String mClassName;
+ final int mIndex;
+ final boolean mFromLayout;
+ final int mFragmentId;
+ final int mContainerId;
+ final String mTag;
+ final boolean mRetainInstance;
+
+ Bundle mSavedFragmentState;
+
+ Fragment mInstance;
+
+ public FragmentState(Fragment frag) {
+ mClassName = frag.getClass().getName();
+ mIndex = frag.mIndex;
+ mFromLayout = frag.mFromLayout;
+ mFragmentId = frag.mFragmentId;
+ mContainerId = frag.mContainerId;
+ mTag = frag.mTag;
+ mRetainInstance = frag.mRetainInstance;
+ }
+
+ public FragmentState(Parcel in) {
+ mClassName = in.readString();
+ mIndex = in.readInt();
+ mFromLayout = in.readInt() != 0;
+ mFragmentId = in.readInt();
+ mContainerId = in.readInt();
+ mTag = in.readString();
+ mRetainInstance = in.readInt() != 0;
+ mSavedFragmentState = in.readBundle();
+ }
+
+ public Fragment instantiate(Activity activity) {
+ if (mInstance != null) {
+ return mInstance;
+ }
+
+ try {
+ mInstance = Fragment.instantiate(activity, mClassName);
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to restore fragment " + mClassName, e);
+ }
+
+ if (mSavedFragmentState != null) {
+ mSavedFragmentState.setClassLoader(activity.getClassLoader());
+ mInstance.mSavedFragmentState = mSavedFragmentState;
+ mInstance.mSavedViewState
+ = mSavedFragmentState.getSparseParcelableArray(VIEW_STATE_TAG);
+ }
+ mInstance.setIndex(mIndex);
+ mInstance.mFromLayout = mFromLayout;
+ mInstance.mFragmentId = mFragmentId;
+ mInstance.mContainerId = mContainerId;
+ mInstance.mTag = mTag;
+ mInstance.mRetainInstance = mRetainInstance;
+
+ return mInstance;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mClassName);
+ dest.writeInt(mIndex);
+ dest.writeInt(mFromLayout ? 1 : 0);
+ dest.writeInt(mFragmentId);
+ dest.writeInt(mContainerId);
+ dest.writeString(mTag);
+ dest.writeInt(mRetainInstance ? 1 : 0);
+ dest.writeBundle(mSavedFragmentState);
+ }
+
+ public static final Parcelable.Creator<FragmentState> CREATOR
+ = new Parcelable.Creator<FragmentState>() {
+ public FragmentState createFromParcel(Parcel in) {
+ return new FragmentState(in);
+ }
+
+ public FragmentState[] newArray(int size) {
+ return new FragmentState[size];
+ }
+ };
+}
+
+/**
+ * A Fragment is a piece of an application's user interface or behavior
+ * that can be placed in an {@link Activity}.
+ */
+public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener {
+ private static final HashMap<String, Class<?>> sClassMap =
+ new HashMap<String, Class<?>>();
+
+ static final int INITIALIZING = 0; // Not yet created.
+ static final int CREATED = 1; // Created.
+ static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.
+ static final int STARTED = 3; // Created and started, not resumed.
+ static final int RESUMED = 4; // Created started and resumed.
+
+ int mState = INITIALIZING;
+
+ // When instantiated from saved state, this is the saved state.
+ Bundle mSavedFragmentState;
+ SparseArray<Parcelable> mSavedViewState;
+
+ // Index into active fragment array.
+ int mIndex = -1;
+
+ // Internal unique name for this fragment;
+ String mWho;
+
+ // True if the fragment is in the list of added fragments.
+ boolean mAdded;
+
+ // True if the fragment is in the resumed state.
+ boolean mResumed;
+
+ // Set to true if this fragment was instantiated from a layout file.
+ boolean mFromLayout;
+
+ // Number of active back stack entries this fragment is in.
+ int mBackStackNesting;
+
+ // Set as soon as a fragment is added to a transaction (or removed),
+ // to be able to do validation.
+ Activity mImmediateActivity;
+
+ // Activity this fragment is attached to.
+ Activity mActivity;
+
+ // The optional identifier for this fragment -- either the container ID if it
+ // was dynamically added to the view hierarchy, or the ID supplied in
+ // layout.
+ int mFragmentId;
+
+ // When a fragment is being dynamically added to the view hierarchy, this
+ // is the identifier of the parent container it is being added to.
+ int mContainerId;
+
+ // The optional named tag for this fragment -- usually used to find
+ // fragments that are not part of the layout.
+ String mTag;
+
+ // Set to true when the app has requested that this fragment be hidden
+ // from the user.
+ boolean mHidden;
+
+ // If set this fragment would like its instance retained across
+ // configuration changes.
+ boolean mRetainInstance;
+
+ // If set this fragment is being retained across the current config change.
+ boolean mRetaining;
+
+ // If set this fragment has menu items to contribute.
+ boolean mHasMenu;
+
+ // Used to verify that subclasses call through to super class.
+ boolean mCalled;
+
+ // If app has requested a specific animation, this is the one to use.
+ int mNextAnim;
+
+ // The parent container of the fragment after dynamically added to UI.
+ ViewGroup mContainer;
+
+ // The View generated for this fragment.
+ View mView;
+
+ LoaderManager mLoaderManager;
+ boolean mStarted;
+
+ /**
+ * Default constructor. <strong>Every</string> fragment must have an
+ * empty constructor, so it can be instantiated when restoring its
+ * activity's state. It is strongly recommended that subclasses do not
+ * have other constructors with parameters, since these constructors
+ * will not be called when the fragment is re-instantiated; instead,
+ * retrieve such parameters from the activity in {@link #onAttach(Activity)}.
+ */
+ public Fragment() {
+ }
+
+ static Fragment instantiate(Activity activity, String fname)
+ throws NoSuchMethodException, ClassNotFoundException,
+ IllegalArgumentException, InstantiationException,
+ IllegalAccessException, InvocationTargetException {
+ Class<?> clazz = sClassMap.get(fname);
+
+ if (clazz == null) {
+ // Class not found in the cache, see if it's real, and try to add it
+ clazz = activity.getClassLoader().loadClass(fname);
+ sClassMap.put(fname, clazz);
+ }
+ return (Fragment)clazz.newInstance();
+ }
+
+ void restoreViewState() {
+ if (mSavedViewState != null) {
+ mView.restoreHierarchyState(mSavedViewState);
+ mSavedViewState = null;
+ }
+ }
+
+ void setIndex(int index) {
+ mIndex = index;
+ mWho = "android:fragment:" + mIndex;
+ }
+
+ void clearIndex() {
+ mIndex = -1;
+ mWho = null;
+ }
+
+ /**
+ * Subclasses can not override equals().
+ */
+ @Override final public boolean equals(Object o) {
+ return super.equals(o);
+ }
+
+ /**
+ * Subclasses can not override hashCode().
+ */
+ @Override final public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Fragment{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ if (mIndex >= 0) {
+ sb.append(" #");
+ sb.append(mIndex);
+ }
+ if (mFragmentId != 0) {
+ sb.append(" id=0x");
+ sb.append(Integer.toHexString(mFragmentId));
+ }
+ if (mTag != null) {
+ sb.append(" ");
+ sb.append(mTag);
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * Return the identifier this fragment is known by. This is either
+ * the android:id value supplied in a layout or the container view ID
+ * supplied when adding the fragment.
+ */
+ final public int getId() {
+ return mFragmentId;
+ }
+
+ /**
+ * Get the tag name of the fragment, if specified.
+ */
+ final public String getTag() {
+ return mTag;
+ }
+
+ /**
+ * Return the Activity this fragment is currently associated with.
+ */
+ final public Activity getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Return true if the fragment is currently added to its activity.
+ */
+ final public boolean isAdded() {
+ return mActivity != null && mActivity.mFragments.mAdded.contains(this);
+ }
+
+ /**
+ * Return true if the fragment is in the resumed state. This is true
+ * for the duration of {@link #onResume()} and {@link #onPause()} as well.
+ */
+ final public boolean isResumed() {
+ return mResumed;
+ }
+
+ /**
+ * Return true if the fragment is currently visible to the user. This means
+ * it: (1) has been added, (2) has its view attached to the window, and
+ * (3) is not hidden.
+ */
+ final public boolean isVisible() {
+ return isAdded() && !isHidden() && mView != null
+ && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * Return true if the fragment has been hidden. By default fragments
+ * are shown. You can find out about changes to this state with
+ * {@link #onHiddenChanged}. Note that the hidden state is orthogonal
+ * to other states -- that is, to be visible to the user, a fragment
+ * must be both started and not hidden.
+ */
+ final public boolean isHidden() {
+ return mHidden;
+ }
+
+ /**
+ * Called when the hidden state (as returned by {@link #isHidden()} of
+ * the fragment has changed. Fragments start out not hidden; this will
+ * be called whenever the fragment changes state from that.
+ * @param hidden True if the fragment is now hidden, false if it is not
+ * visible.
+ */
+ public void onHiddenChanged(boolean hidden) {
+ }
+
+ /**
+ * Control whether a fragment instance is retained across Activity
+ * re-creation (such as from a configuration change). This can only
+ * be used with fragments not in the back stack. If set, the fragment
+ * lifecycle will be slightly different when an activity is recreated:
+ * <ul>
+ * <li> {@link #onDestroy()} will not be called (but {@link #onDetach()} still
+ * will be, because the fragment is being detached from its current activity).
+ * <li> {@link #onCreate(Bundle)} will not be called since the fragment
+ * is not being re-created.
+ * <li> {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} <b>will</b>
+ * still be called.
+ * </ul>
+ */
+ public void setRetainInstance(boolean retain) {
+ mRetainInstance = retain;
+ }
+
+ final public boolean getRetainInstance() {
+ return mRetainInstance;
+ }
+
+ /**
+ * Report that this fragment would like to participate in populating
+ * the options menu by receiving a call to {@link #onCreateOptionsMenu}
+ * and related methods.
+ *
+ * @param hasMenu If true, the fragment has menu items to contribute.
+ */
+ public void setHasOptionsMenu(boolean hasMenu) {
+ if (mHasMenu != hasMenu) {
+ mHasMenu = hasMenu;
+ if (isAdded() && !isHidden()) {
+ mActivity.invalidateOptionsMenu();
+ }
+ }
+ }
+
+ /**
+ * Return the LoaderManager for this fragment, creating it if needed.
+ */
+ public LoaderManager getLoaderManager() {
+ if (mLoaderManager != null) {
+ return mLoaderManager;
+ }
+ mLoaderManager = mActivity.getLoaderManager(mIndex, mStarted);
+ return mLoaderManager;
+ }
+
+ /**
+ * Call {@link Activity#startActivity(Intent)} on the fragment's
+ * containing Activity.
+ */
+ public void startActivity(Intent intent) {
+ mActivity.startActivityFromFragment(this, intent, -1);
+ }
+
+ /**
+ * Call {@link Activity#startActivityForResult(Intent, int)} on the fragment's
+ * containing Activity.
+ */
+ public void startActivityForResult(Intent intent, int requestCode) {
+ mActivity.startActivityFromFragment(this, intent, requestCode);
+ }
+
+ /**
+ * Receive the result from a previous call to
+ * {@link #startActivityForResult(Intent, int)}. This follows the
+ * related Activity API as described there in
+ * {@link Activity#onActivityResult(int, int, Intent)}.
+ *
+ * @param requestCode The integer request code originally supplied to
+ * startActivityForResult(), allowing you to identify who this
+ * result came from.
+ * @param resultCode The integer result code returned by the child activity
+ * through its setResult().
+ * @param data An Intent, which can return result data to the caller
+ * (various data can be attached to Intent "extras").
+ */
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ /**
+ * Called when a fragment is being created as part of a view layout
+ * inflation, typically from setting the content view of an activity. This
+ * will be called both the first time the fragment is created, as well
+ * later when it is being re-created from its saved state (which is also
+ * given here).
+ *
+ * XXX This is kind-of yucky... maybe we could just supply the
+ * AttributeSet to onCreate()?
+ *
+ * @param activity The Activity that is inflating the fragment.
+ * @param attrs The attributes at the tag where the fragment is
+ * being created.
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ public void onInflate(Activity activity, AttributeSet attrs,
+ Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when a fragment is first attached to its activity.
+ * {@link #onCreate(Bundle)} will be called after this.
+ */
+ public void onAttach(Activity activity) {
+ mCalled = true;
+ }
+
+ public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
+ return null;
+ }
+
+ /**
+ * Called to do initial creation of a fragment. This is called after
+ * {@link #onAttach(Activity)} and before
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
+ *
+ * <p>Note that this can be called while the fragment's activity is
+ * still in the process of being created. As such, you can not rely
+ * on things like the activity's content view hierarchy being initialized
+ * at this point. If you want to do work once the activity itself is
+ * created, see {@link #onActivityCreated(Bundle)}.
+ *
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ public void onCreate(Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called to have the fragment instantiate its user interface view.
+ * This is optional, and non-graphical fragments can return null (which
+ * is the default implementation). This will be called between
+ * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.
+ *
+ * <p>If you return a View from here, you will later be called in
+ * {@link #onDestroyView} when the view is being released.
+ *
+ * @param inflater The LayoutInflater object that can be used to inflate
+ * any views in the fragment,
+ * @param container If non-null, this is the parent view that the fragment's
+ * UI should be attached to. The fragment should not add the view itself,
+ * but this can be used to generate the LayoutParams of the view.
+ * @param savedInstanceState If non-null, this fragment is being re-constructed
+ * from a previous saved state as given here.
+ *
+ * @return Return the View for the fragment's UI, or null.
+ */
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return null;
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ /**
+ * Called when the fragment's activity has been created and this
+ * fragment's view hierarchy instantiated. It can be used to do final
+ * initialization once these pieces are in place, such as retrieving
+ * views or restoring state. It is also useful for fragments that use
+ * {@link #setRetainInstance(boolean)} to retain their instance,
+ * as this callback tells the fragment when it is fully associated with
+ * the new activity instance. This is called after {@link #onCreateView}
+ * and before {@link #onStart()}.
+ *
+ * @param savedInstanceState If the fragment is being re-created from
+ * a previous saved state, this is the state.
+ */
+ public void onActivityCreated(Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the Fragment is visible to the user. This is generally
+ * tied to {@link Activity#onStart() Activity.onStart} of the containing
+ * Activity's lifecycle.
+ */
+ public void onStart() {
+ mCalled = true;
+ mStarted = true;
+ if (mLoaderManager != null) {
+ mLoaderManager.doStart();
+ }
+ }
+
+ /**
+ * Called when the fragment is visible to the user and actively running.
+ * This is generally
+ * tied to {@link Activity#onResume() Activity.onResume} of the containing
+ * Activity's lifecycle.
+ */
+ public void onResume() {
+ mCalled = true;
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the Fragment is no longer resumed. This is generally
+ * tied to {@link Activity#onPause() Activity.onPause} of the containing
+ * Activity's lifecycle.
+ */
+ public void onPause() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the Fragment is no longer started. This is generally
+ * tied to {@link Activity#onStop() Activity.onStop} of the containing
+ * Activity's lifecycle.
+ */
+ public void onStop() {
+ mCalled = true;
+ }
+
+ public void onLowMemory() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the view previously created by {@link #onCreateView} has
+ * been detached from the fragment. The next time the fragment needs
+ * to be displayed, a new view will be created. This is called
+ * after {@link #onStop()} and before {@link #onDestroy()}; it is only
+ * called if {@link #onCreateView} returns a non-null View.
+ */
+ public void onDestroyView() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when the fragment is no longer in use. This is called
+ * after {@link #onStop()} and before {@link #onDetach()}.
+ */
+ public void onDestroy() {
+ mCalled = true;
+ if (mLoaderManager != null) {
+ mLoaderManager.doDestroy();
+ }
+ }
+
+ /**
+ * Called when the fragment is no longer attached to its activity. This
+ * is called after {@link #onDestroy()}.
+ */
+ public void onDetach() {
+ mCalled = true;
+ }
+
+ /**
+ * Initialize the contents of the Activity's standard options menu. You
+ * should place your menu items in to <var>menu</var>. For this method
+ * to be called, you must have first called {@link #setHasOptionsMenu}. See
+ * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu}
+ * for more information.
+ *
+ * @param menu The options menu in which you place your items.
+ *
+ * @see #setHasOptionsMenu
+ * @see #onPrepareOptionsMenu
+ * @see #onOptionsItemSelected
+ */
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ }
+
+ /**
+ * Prepare the Screen's standard options menu to be displayed. This is
+ * called right before the menu is shown, every time it is shown. You can
+ * use this method to efficiently enable/disable items or otherwise
+ * dynamically modify the contents. See
+ * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu}
+ * for more information.
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ *
+ * @see #setHasOptionsMenu
+ * @see #onCreateOptionsMenu
+ */
+ public void onPrepareOptionsMenu(Menu menu) {
+ }
+
+ /**
+ * This hook is called whenever an item in your options menu is selected.
+ * The default implementation simply returns false to have the normal
+ * processing happen (calling the item's Runnable or sending a message to
+ * its Handler as appropriate). You can use this method for any items
+ * for which you would like to do processing without those other
+ * facilities.
+ *
+ * <p>Derived classes should call through to the base class for it to
+ * perform the default menu handling.
+ *
+ * @param item The menu item that was selected.
+ *
+ * @return boolean Return false to allow normal menu processing to
+ * proceed, true to consume it here.
+ *
+ * @see #onCreateOptionsMenu
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return false;
+ }
+
+ /**
+ * This hook is called whenever the options menu is being closed (either by the user canceling
+ * the menu with the back/menu button, or when an item is selected).
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ */
+ public void onOptionsMenuClosed(Menu menu) {
+ }
+
+ /**
+ * Called when a context menu for the {@code view} is about to be shown.
+ * Unlike {@link #onCreateOptionsMenu}, this will be called every
+ * time the context menu is about to be shown and should be populated for
+ * the view (or item inside the view for {@link AdapterView} subclasses,
+ * this can be found in the {@code menuInfo})).
+ * <p>
+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an
+ * item has been selected.
+ * <p>
+ * The default implementation calls up to
+ * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though
+ * you can not call this implementation if you don't want that behavior.
+ * <p>
+ * It is not safe to hold onto the context menu after this method returns.
+ * {@inheritDoc}
+ */
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ getActivity().onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ /**
+ * Registers a context menu to be shown for the given view (multiple views
+ * can show the context menu). This method will set the
+ * {@link OnCreateContextMenuListener} on the view to this fragment, so
+ * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be
+ * called when it is time to show the context menu.
+ *
+ * @see #unregisterForContextMenu(View)
+ * @param view The view that should show a context menu.
+ */
+ public void registerForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(this);
+ }
+
+ /**
+ * Prevents a context menu to be shown for the given view. This method will
+ * remove the {@link OnCreateContextMenuListener} on the view.
+ *
+ * @see #registerForContextMenu(View)
+ * @param view The view that should stop showing a context menu.
+ */
+ public void unregisterForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(null);
+ }
+
+ /**
+ * This hook is called whenever an item in a context menu is selected. The
+ * default implementation simply returns false to have the normal processing
+ * happen (calling the item's Runnable or sending a message to its Handler
+ * as appropriate). You can use this method for any items for which you
+ * would like to do processing without those other facilities.
+ * <p>
+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the
+ * View that added this menu item.
+ * <p>
+ * Derived classes should call through to the base class for it to perform
+ * the default menu handling.
+ *
+ * @param item The context menu item that was selected.
+ * @return boolean Return false to allow normal context menu processing to
+ * proceed, true to consume it here.
+ */
+ public boolean onContextItemSelected(MenuItem item) {
+ return false;
+ }
+
+ void performStop() {
+ onStop();
+ if (mStarted) {
+ mStarted = false;
+ if (mLoaderManager != null) {
+ if (mActivity == null || !mActivity.mChangingConfigurations) {
+ mLoaderManager.doStop();
+ } else {
+ mLoaderManager.doRetain();
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java
new file mode 100644
index 0000000..4f3043c
--- /dev/null
+++ b/core/java/android/app/FragmentManager.java
@@ -0,0 +1,1000 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+import java.util.ArrayList;
+
+final class FragmentManagerState implements Parcelable {
+ FragmentState[] mActive;
+ int[] mAdded;
+ BackStackState[] mBackStack;
+
+ public FragmentManagerState() {
+ }
+
+ public FragmentManagerState(Parcel in) {
+ mActive = in.createTypedArray(FragmentState.CREATOR);
+ mAdded = in.createIntArray();
+ mBackStack = in.createTypedArray(BackStackState.CREATOR);
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedArray(mActive, flags);
+ dest.writeIntArray(mAdded);
+ dest.writeTypedArray(mBackStack, flags);
+ }
+
+ public static final Parcelable.Creator<FragmentManagerState> CREATOR
+ = new Parcelable.Creator<FragmentManagerState>() {
+ public FragmentManagerState createFromParcel(Parcel in) {
+ return new FragmentManagerState(in);
+ }
+
+ public FragmentManagerState[] newArray(int size) {
+ return new FragmentManagerState[size];
+ }
+ };
+}
+
+/**
+ * @hide
+ * Container for fragments associated with an activity.
+ */
+public class FragmentManager {
+ static final boolean DEBUG = true;
+ static final String TAG = "FragmentManager";
+
+ ArrayList<Runnable> mPendingActions;
+ Runnable[] mTmpActions;
+ boolean mExecutingActions;
+
+ ArrayList<Fragment> mActive;
+ ArrayList<Fragment> mAdded;
+ ArrayList<Integer> mAvailIndices;
+ ArrayList<BackStackEntry> mBackStack;
+
+ int mCurState = Fragment.INITIALIZING;
+ Activity mActivity;
+
+ boolean mNeedMenuInvalidate;
+
+ // Temporary vars for state save and restore.
+ Bundle mStateBundle = null;
+ SparseArray<Parcelable> mStateArray = null;
+
+ Runnable mExecCommit = new Runnable() {
+ @Override
+ public void run() {
+ execPendingActions();
+ }
+ };
+
+ Animation loadAnimation(Fragment fragment, int transit, boolean enter,
+ int transitionStyle) {
+ Animation animObj = fragment.onCreateAnimation(transitionStyle, enter,
+ fragment.mNextAnim);
+ if (animObj != null) {
+ return animObj;
+ }
+
+ if (fragment.mNextAnim != 0) {
+ Animation anim = AnimationUtils.loadAnimation(mActivity, fragment.mNextAnim);
+ if (anim != null) {
+ return anim;
+ }
+ }
+
+ if (transit == 0) {
+ return null;
+ }
+
+ int styleIndex = transitToStyleIndex(transit, enter);
+ if (styleIndex < 0) {
+ return null;
+ }
+
+ if (transitionStyle == 0 && mActivity.getWindow() != null) {
+ transitionStyle = mActivity.getWindow().getAttributes().windowAnimations;
+ }
+ if (transitionStyle == 0) {
+ return null;
+ }
+
+ TypedArray attrs = mActivity.obtainStyledAttributes(transitionStyle,
+ com.android.internal.R.styleable.WindowAnimation);
+ int anim = attrs.getResourceId(styleIndex, 0);
+ attrs.recycle();
+
+ if (anim == 0) {
+ return null;
+ }
+
+ return AnimationUtils.loadAnimation(mActivity, anim);
+ }
+
+ void moveToState(Fragment f, int newState, int transit, int transitionStyle) {
+ // Fragments that are not currently added will sit in the onCreate() state.
+ if (!f.mAdded && newState > Fragment.CREATED) {
+ newState = Fragment.CREATED;
+ }
+
+ if (f.mState < newState) {
+ switch (f.mState) {
+ case Fragment.INITIALIZING:
+ if (DEBUG) Log.v(TAG, "moveto CREATED: " + f);
+ f.mActivity = mActivity;
+ f.mCalled = false;
+ f.onAttach(mActivity);
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onAttach()");
+ }
+ mActivity.onAttachFragment(f);
+
+ if (!f.mRetaining) {
+ f.mCalled = false;
+ f.onCreate(f.mSavedFragmentState);
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onCreate()");
+ }
+ }
+ f.mRetaining = false;
+ if (f.mFromLayout) {
+ // For fragments that are part of the content view
+ // layout, we need to instantiate the view immediately
+ // and the inflater will take care of adding it.
+ f.mView = f.onCreateView(mActivity.getLayoutInflater(),
+ null, f.mSavedFragmentState);
+ if (f.mView != null) {
+ f.mView.setSaveFromParentEnabled(false);
+ f.restoreViewState();
+ if (f.mHidden) f.mView.setVisibility(View.GONE);
+ }
+ }
+ case Fragment.CREATED:
+ if (newState > Fragment.CREATED) {
+ if (DEBUG) Log.v(TAG, "moveto CONTENT: " + f);
+ if (!f.mFromLayout) {
+ ViewGroup container = null;
+ if (f.mContainerId != 0) {
+ container = (ViewGroup)mActivity.findViewById(f.mContainerId);
+ if (container == null) {
+ throw new IllegalArgumentException("New view found for id 0x"
+ + Integer.toHexString(f.mContainerId)
+ + " for fragment " + f);
+ }
+ }
+ f.mContainer = container;
+ f.mView = f.onCreateView(mActivity.getLayoutInflater(),
+ container, f.mSavedFragmentState);
+ if (f.mView != null) {
+ f.mView.setSaveFromParentEnabled(false);
+ if (container != null) {
+ Animation anim = loadAnimation(f, transit, true,
+ transitionStyle);
+ if (anim != null) {
+ f.mView.setAnimation(anim);
+ }
+ container.addView(f.mView);
+ f.restoreViewState();
+ }
+ if (f.mHidden) f.mView.setVisibility(View.GONE);
+ }
+ }
+
+ f.mCalled = false;
+ f.onActivityCreated(f.mSavedFragmentState);
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onReady()");
+ }
+ f.mSavedFragmentState = null;
+ }
+ case Fragment.ACTIVITY_CREATED:
+ if (newState > Fragment.ACTIVITY_CREATED) {
+ if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);
+ f.mCalled = false;
+ f.onStart();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onStart()");
+ }
+ }
+ case Fragment.STARTED:
+ if (newState > Fragment.STARTED) {
+ if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);
+ f.mCalled = false;
+ f.mResumed = true;
+ f.onResume();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onResume()");
+ }
+ }
+ }
+ } else if (f.mState > newState) {
+ switch (f.mState) {
+ case Fragment.RESUMED:
+ if (newState < Fragment.RESUMED) {
+ if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f);
+ f.mCalled = false;
+ f.onPause();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onPause()");
+ }
+ f.mResumed = false;
+ }
+ case Fragment.STARTED:
+ if (newState < Fragment.STARTED) {
+ if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f);
+ f.mCalled = false;
+ f.performStop();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onStop()");
+ }
+ }
+ case Fragment.ACTIVITY_CREATED:
+ if (newState < Fragment.ACTIVITY_CREATED) {
+ if (DEBUG) Log.v(TAG, "movefrom CONTENT: " + f);
+ if (f.mView != null) {
+ f.mCalled = false;
+ f.onDestroyView();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onDestroyedView()");
+ }
+ // Need to save the current view state if not
+ // done already.
+ if (!mActivity.isFinishing() && f.mSavedFragmentState == null) {
+ saveFragmentViewState(f);
+ }
+ if (f.mContainer != null) {
+ if (mCurState > Fragment.INITIALIZING) {
+ Animation anim = loadAnimation(f, transit, false,
+ transitionStyle);
+ if (anim != null) {
+ f.mView.setAnimation(anim);
+ }
+ }
+ f.mContainer.removeView(f.mView);
+ }
+ }
+ f.mContainer = null;
+ f.mView = null;
+ }
+ case Fragment.CREATED:
+ if (newState < Fragment.CREATED) {
+ if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f);
+ if (!f.mRetaining) {
+ f.mCalled = false;
+ f.onDestroy();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onDestroy()");
+ }
+ }
+
+ f.mCalled = false;
+ f.onDetach();
+ if (!f.mCalled) {
+ throw new SuperNotCalledException("Fragment " + f
+ + " did not call through to super.onDetach()");
+ }
+ f.mActivity = null;
+ }
+ }
+ }
+
+ f.mState = newState;
+ }
+
+ void moveToState(int newState, boolean always) {
+ moveToState(newState, 0, 0, always);
+ }
+
+ void moveToState(int newState, int transit, int transitStyle, boolean always) {
+ if (mActivity == null && newState != Fragment.INITIALIZING) {
+ throw new IllegalStateException("No activity");
+ }
+
+ if (!always && mCurState == newState) {
+ return;
+ }
+
+ mCurState = newState;
+ if (mActive != null) {
+ for (int i=0; i<mActive.size(); i++) {
+ Fragment f = mActive.get(i);
+ if (f != null) {
+ moveToState(f, newState, transit, transitStyle);
+ }
+ }
+ }
+ }
+
+ void makeActive(Fragment f) {
+ if (f.mIndex >= 0) {
+ return;
+ }
+
+ if (mAvailIndices == null || mAvailIndices.size() <= 0) {
+ if (mActive == null) {
+ mActive = new ArrayList<Fragment>();
+ }
+ f.setIndex(mActive.size());
+ mActive.add(f);
+
+ } else {
+ f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1));
+ mActive.set(f.mIndex, f);
+ }
+ }
+
+ void makeInactive(Fragment f) {
+ if (f.mIndex < 0) {
+ return;
+ }
+
+ mActive.set(f.mIndex, null);
+ if (mAvailIndices == null) {
+ mAvailIndices = new ArrayList<Integer>();
+ }
+ mAvailIndices.add(f.mIndex);
+ mActivity.invalidateFragmentIndex(f.mIndex);
+ f.clearIndex();
+ }
+
+ public void addFragment(Fragment fragment, boolean moveToStateNow) {
+ if (DEBUG) Log.v(TAG, "add: " + fragment);
+ if (mAdded == null) {
+ mAdded = new ArrayList<Fragment>();
+ }
+ mAdded.add(fragment);
+ makeActive(fragment);
+ fragment.mAdded = true;
+ if (fragment.mHasMenu) {
+ mNeedMenuInvalidate = true;
+ }
+ if (moveToStateNow) {
+ moveToState(fragment, mCurState, 0, 0);
+ }
+ }
+
+ public void removeFragment(Fragment fragment, int transition, int transitionStyle) {
+ if (DEBUG) Log.v(TAG, "remove: " + fragment);
+ mAdded.remove(fragment);
+ final boolean inactive = fragment.mBackStackNesting <= 0;
+ if (inactive) {
+ makeInactive(fragment);
+ }
+ if (fragment.mHasMenu) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.mAdded = false;
+ moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,
+ transition, transitionStyle);
+ }
+
+ public void hideFragment(Fragment fragment, int transition, int transitionStyle) {
+ if (DEBUG) Log.v(TAG, "hide: " + fragment);
+ if (!fragment.mHidden) {
+ fragment.mHidden = true;
+ if (fragment.mView != null) {
+ Animation anim = loadAnimation(fragment, transition, false,
+ transitionStyle);
+ if (anim != null) {
+ fragment.mView.setAnimation(anim);
+ }
+ fragment.mView.setVisibility(View.GONE);
+ }
+ if (fragment.mAdded && fragment.mHasMenu) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.onHiddenChanged(true);
+ }
+ }
+
+ public void showFragment(Fragment fragment, int transition, int transitionStyle) {
+ if (DEBUG) Log.v(TAG, "show: " + fragment);
+ if (fragment.mHidden) {
+ fragment.mHidden = false;
+ if (fragment.mView != null) {
+ Animation anim = loadAnimation(fragment, transition, true,
+ transitionStyle);
+ if (anim != null) {
+ fragment.mView.setAnimation(anim);
+ }
+ fragment.mView.setVisibility(View.VISIBLE);
+ }
+ if (fragment.mAdded && fragment.mHasMenu) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.onHiddenChanged(false);
+ }
+ }
+
+ public Fragment findFragmentById(int id) {
+ if (mActive != null) {
+ // First look through added fragments.
+ for (int i=mAdded.size()-1; i>=0; i--) {
+ Fragment f = mAdded.get(i);
+ if (f != null && f.mFragmentId == id) {
+ return f;
+ }
+ }
+ // Now for any known fragment.
+ for (int i=mActive.size()-1; i>=0; i--) {
+ Fragment f = mActive.get(i);
+ if (f != null && f.mFragmentId == id) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Fragment findFragmentByTag(String tag) {
+ if (mActive != null && tag != null) {
+ // First look through added fragments.
+ for (int i=mAdded.size()-1; i>=0; i--) {
+ Fragment f = mAdded.get(i);
+ if (f != null && tag.equals(f.mTag)) {
+ return f;
+ }
+ }
+ // Now for any known fragment.
+ for (int i=mActive.size()-1; i>=0; i--) {
+ Fragment f = mActive.get(i);
+ if (f != null && tag.equals(f.mTag)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Fragment findFragmentByWho(String who) {
+ if (mActive != null && who != null) {
+ for (int i=mActive.size()-1; i>=0; i--) {
+ Fragment f = mActive.get(i);
+ if (f != null && who.equals(f.mWho)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void enqueueAction(Runnable action) {
+ synchronized (this) {
+ if (mPendingActions == null) {
+ mPendingActions = new ArrayList<Runnable>();
+ }
+ mPendingActions.add(action);
+ if (mPendingActions.size() == 1) {
+ mActivity.mHandler.removeCallbacks(mExecCommit);
+ mActivity.mHandler.post(mExecCommit);
+ }
+ }
+ }
+
+ /**
+ * Only call from main thread!
+ */
+ public void execPendingActions() {
+ if (mExecutingActions) {
+ throw new IllegalStateException("Recursive entry to execPendingActions");
+ }
+
+ while (true) {
+ int numActions;
+
+ synchronized (this) {
+ if (mPendingActions == null || mPendingActions.size() == 0) {
+ return;
+ }
+
+ numActions = mPendingActions.size();
+ if (mTmpActions == null || mTmpActions.length < numActions) {
+ mTmpActions = new Runnable[numActions];
+ }
+ mPendingActions.toArray(mTmpActions);
+ mPendingActions.clear();
+ mActivity.mHandler.removeCallbacks(mExecCommit);
+ }
+
+ mExecutingActions = true;
+ for (int i=0; i<numActions; i++) {
+ mTmpActions[i].run();
+ }
+ mExecutingActions = false;
+ }
+ }
+
+ public void addBackStackState(BackStackEntry state) {
+ if (mBackStack == null) {
+ mBackStack = new ArrayList<BackStackEntry>();
+ }
+ mBackStack.add(state);
+ }
+
+ public boolean popBackStackState(Handler handler, String name) {
+ if (mBackStack == null) {
+ return false;
+ }
+ if (name == null) {
+ int last = mBackStack.size()-1;
+ if (last < 0) {
+ return false;
+ }
+ final BackStackEntry bss = mBackStack.remove(last);
+ enqueueAction(new Runnable() {
+ public void run() {
+ if (DEBUG) Log.v(TAG, "Popping back stack state: " + bss);
+ bss.popFromBackStack();
+ moveToState(mCurState, reverseTransit(bss.getTransition()),
+ bss.getTransitionStyle(), true);
+ }
+ });
+ } else {
+ int index = mBackStack.size()-1;
+ while (index >= 0) {
+ BackStackEntry bss = mBackStack.get(index);
+ if (name.equals(bss.getName())) {
+ break;
+ }
+ }
+ if (index < 0 || index == mBackStack.size()-1) {
+ return false;
+ }
+ final ArrayList<BackStackEntry> states
+ = new ArrayList<BackStackEntry>();
+ for (int i=mBackStack.size()-1; i>index; i--) {
+ states.add(mBackStack.remove(i));
+ }
+ enqueueAction(new Runnable() {
+ public void run() {
+ for (int i=0; i<states.size(); i++) {
+ if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i));
+ states.get(i).popFromBackStack();
+ }
+ moveToState(mCurState, true);
+ }
+ });
+ }
+ return true;
+ }
+
+ ArrayList<Fragment> retainNonConfig() {
+ ArrayList<Fragment> fragments = null;
+ if (mActive != null) {
+ for (int i=0; i<mActive.size(); i++) {
+ Fragment f = mActive.get(i);
+ if (f != null && f.mRetainInstance) {
+ if (fragments == null) {
+ fragments = new ArrayList<Fragment>();
+ }
+ fragments.add(f);
+ f.mRetaining = true;
+ }
+ }
+ }
+ return fragments;
+ }
+
+ void saveFragmentViewState(Fragment f) {
+ if (f.mView == null) {
+ return;
+ }
+ if (mStateArray == null) {
+ mStateArray = new SparseArray<Parcelable>();
+ }
+ f.mView.saveHierarchyState(mStateArray);
+ if (mStateArray.size() > 0) {
+ f.mSavedViewState = mStateArray;
+ mStateArray = null;
+ }
+ }
+
+ Parcelable saveAllState() {
+ if (mActive == null || mActive.size() <= 0) {
+ return null;
+ }
+
+ // First collect all active fragments.
+ int N = mActive.size();
+ FragmentState[] active = new FragmentState[N];
+ boolean haveFragments = false;
+ for (int i=0; i<N; i++) {
+ Fragment f = mActive.get(i);
+ if (f != null) {
+ haveFragments = true;
+
+ FragmentState fs = new FragmentState(f);
+ active[i] = fs;
+
+ if (mStateBundle == null) {
+ mStateBundle = new Bundle();
+ }
+ f.onSaveInstanceState(mStateBundle);
+ if (!mStateBundle.isEmpty()) {
+ fs.mSavedFragmentState = mStateBundle;
+ mStateBundle = null;
+ }
+
+ if (f.mView != null) {
+ saveFragmentViewState(f);
+ if (f.mSavedViewState != null) {
+ if (fs.mSavedFragmentState == null) {
+ fs.mSavedFragmentState = new Bundle();
+ }
+ fs.mSavedFragmentState.putSparseParcelableArray(
+ FragmentState.VIEW_STATE_TAG, f.mSavedViewState);
+ }
+ }
+
+ }
+ }
+
+ if (!haveFragments) {
+ return null;
+ }
+
+ int[] added = null;
+ BackStackState[] backStack = null;
+
+ // Build list of currently added fragments.
+ N = mAdded.size();
+ if (N > 0) {
+ added = new int[N];
+ for (int i=0; i<N; i++) {
+ added[i] = mAdded.get(i).mIndex;
+ }
+ }
+
+ // Now save back stack.
+ if (mBackStack != null) {
+ N = mBackStack.size();
+ if (N > 0) {
+ backStack = new BackStackState[N];
+ for (int i=0; i<N; i++) {
+ backStack[i] = new BackStackState(this, mBackStack.get(i));
+ }
+ }
+ }
+
+ FragmentManagerState fms = new FragmentManagerState();
+ fms.mActive = active;
+ fms.mAdded = added;
+ fms.mBackStack = backStack;
+ return fms;
+ }
+
+ void restoreAllState(Parcelable state, ArrayList<Fragment> nonConfig) {
+ // If there is no saved state at all, then there can not be
+ // any nonConfig fragments either, so that is that.
+ if (state == null) return;
+ FragmentManagerState fms = (FragmentManagerState)state;
+ if (fms.mActive == null) return;
+
+ // First re-attach any non-config instances we are retaining back
+ // to their saved state, so we don't try to instantiate them again.
+ if (nonConfig != null) {
+ for (int i=0; i<nonConfig.size(); i++) {
+ Fragment f = nonConfig.get(i);
+ FragmentState fs = fms.mActive[f.mIndex];
+ fs.mInstance = f;
+ f.mSavedViewState = null;
+ f.mBackStackNesting = 0;
+ f.mAdded = false;
+ if (fs.mSavedFragmentState != null) {
+ f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(
+ FragmentState.VIEW_STATE_TAG);
+ }
+ }
+ }
+
+ // Build the full list of active fragments, instantiating them from
+ // their saved state.
+ mActive = new ArrayList<Fragment>(fms.mActive.length);
+ if (mAvailIndices != null) {
+ mAvailIndices.clear();
+ }
+ for (int i=0; i<fms.mActive.length; i++) {
+ FragmentState fs = fms.mActive[i];
+ if (fs != null) {
+ mActive.add(fs.instantiate(mActivity));
+ } else {
+ mActive.add(null);
+ if (mAvailIndices == null) {
+ mAvailIndices = new ArrayList<Integer>();
+ }
+ mAvailIndices.add(i);
+ }
+ }
+
+ // Build the list of currently added fragments.
+ if (fms.mAdded != null) {
+ mAdded = new ArrayList<Fragment>(fms.mAdded.length);
+ for (int i=0; i<fms.mAdded.length; i++) {
+ Fragment f = mActive.get(fms.mAdded[i]);
+ if (f == null) {
+ throw new IllegalStateException(
+ "No instantiated fragment for index #" + fms.mAdded[i]);
+ }
+ f.mAdded = true;
+ f.mImmediateActivity = mActivity;
+ mAdded.add(f);
+ }
+ } else {
+ mAdded = null;
+ }
+
+ // Build the back stack.
+ if (fms.mBackStack != null) {
+ mBackStack = new ArrayList<BackStackEntry>(fms.mBackStack.length);
+ for (int i=0; i<fms.mBackStack.length; i++) {
+ BackStackEntry bse = fms.mBackStack[i].instantiate(this);
+ mBackStack.add(bse);
+ }
+ } else {
+ mBackStack = null;
+ }
+ }
+
+ public void attachActivity(Activity activity) {
+ if (mActivity != null) throw new IllegalStateException();
+ mActivity = activity;
+ }
+
+ public void dispatchCreate() {
+ moveToState(Fragment.CREATED, false);
+ }
+
+ public void dispatchActivityCreated() {
+ moveToState(Fragment.ACTIVITY_CREATED, false);
+ }
+
+ public void dispatchStart() {
+ moveToState(Fragment.STARTED, false);
+ }
+
+ public void dispatchResume() {
+ moveToState(Fragment.RESUMED, false);
+ }
+
+ public void dispatchPause() {
+ moveToState(Fragment.STARTED, false);
+ }
+
+ public void dispatchStop() {
+ moveToState(Fragment.ACTIVITY_CREATED, false);
+ }
+
+ public void dispatchDestroy() {
+ moveToState(Fragment.INITIALIZING, false);
+ mActivity = null;
+ }
+
+ public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ boolean show = false;
+ if (mActive != null) {
+ for (int i=0; i<mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null && !f.mHidden && f.mHasMenu) {
+ show = true;
+ f.onCreateOptionsMenu(menu, inflater);
+ }
+ }
+ }
+ return show;
+ }
+
+ public boolean dispatchPrepareOptionsMenu(Menu menu) {
+ boolean show = false;
+ if (mActive != null) {
+ for (int i=0; i<mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null && !f.mHidden && f.mHasMenu) {
+ show = true;
+ f.onPrepareOptionsMenu(menu);
+ }
+ }
+ }
+ return show;
+ }
+
+ public boolean dispatchOptionsItemSelected(MenuItem item) {
+ if (mActive != null) {
+ for (int i=0; i<mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null && !f.mHidden && f.mHasMenu) {
+ if (f.onOptionsItemSelected(item)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean dispatchContextItemSelected(MenuItem item) {
+ if (mActive != null) {
+ for (int i=0; i<mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null && !f.mHidden) {
+ if (f.onContextItemSelected(item)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public void dispatchOptionsMenuClosed(Menu menu) {
+ if (mActive != null) {
+ for (int i=0; i<mAdded.size(); i++) {
+ Fragment f = mAdded.get(i);
+ if (f != null && !f.mHidden && f.mHasMenu) {
+ f.onOptionsMenuClosed(menu);
+ }
+ }
+ }
+ }
+
+ public static int reverseTransit(int transit) {
+ int rev = 0;
+ switch (transit) {
+ case FragmentTransaction.TRANSIT_ENTER:
+ rev = FragmentTransaction.TRANSIT_EXIT;
+ break;
+ case FragmentTransaction.TRANSIT_EXIT:
+ rev = FragmentTransaction.TRANSIT_ENTER;
+ break;
+ case FragmentTransaction.TRANSIT_SHOW:
+ rev = FragmentTransaction.TRANSIT_HIDE;
+ break;
+ case FragmentTransaction.TRANSIT_HIDE:
+ rev = FragmentTransaction.TRANSIT_SHOW;
+ break;
+ case FragmentTransaction.TRANSIT_ACTIVITY_OPEN:
+ rev = FragmentTransaction.TRANSIT_ACTIVITY_CLOSE;
+ break;
+ case FragmentTransaction.TRANSIT_ACTIVITY_CLOSE:
+ rev = FragmentTransaction.TRANSIT_ACTIVITY_OPEN;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_OPEN:
+ rev = FragmentTransaction.TRANSIT_TASK_CLOSE;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_CLOSE:
+ rev = FragmentTransaction.TRANSIT_TASK_OPEN;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_TO_FRONT:
+ rev = FragmentTransaction.TRANSIT_TASK_TO_BACK;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_TO_BACK:
+ rev = FragmentTransaction.TRANSIT_TASK_TO_FRONT;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_OPEN:
+ rev = FragmentTransaction.TRANSIT_WALLPAPER_CLOSE;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_CLOSE:
+ rev = FragmentTransaction.TRANSIT_WALLPAPER_OPEN;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN:
+ rev = FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE:
+ rev = FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN;
+ break;
+ }
+ return rev;
+
+ }
+
+ public static int transitToStyleIndex(int transit, boolean enter) {
+ int animAttr = -1;
+ switch (transit) {
+ case FragmentTransaction.TRANSIT_ENTER:
+ animAttr = com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_EXIT:
+ animAttr = com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_SHOW:
+ animAttr = com.android.internal.R.styleable.WindowAnimation_windowShowAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_HIDE:
+ animAttr = com.android.internal.R.styleable.WindowAnimation_windowHideAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_ACTIVITY_OPEN:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_activityOpenEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_activityOpenExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_ACTIVITY_CLOSE:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_OPEN:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_taskOpenEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_taskOpenExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_CLOSE:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_taskCloseEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_taskCloseExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_TO_FRONT:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_taskToFrontEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_taskToFrontExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_TASK_TO_BACK:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_taskToBackEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_taskToBackExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_OPEN:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_CLOSE:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_wallpaperCloseEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_wallpaperCloseExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_OPEN:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation;
+ break;
+ case FragmentTransaction.TRANSIT_WALLPAPER_INTRA_CLOSE:
+ animAttr = enter
+ ? com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation;
+ break;
+ }
+ return animAttr;
+ }
+}
diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java
new file mode 100644
index 0000000..840f274
--- /dev/null
+++ b/core/java/android/app/FragmentTransaction.java
@@ -0,0 +1,151 @@
+package android.app;
+
+/**
+ * API for performing a set of Fragment operations.
+ */
+public interface FragmentTransaction {
+ /**
+ * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId.
+ */
+ public FragmentTransaction add(Fragment fragment, String tag);
+
+ /**
+ * Calls {@link #add(int, Fragment, String)} with a null tag.
+ */
+ public FragmentTransaction add(int containerViewId, Fragment fragment);
+
+ /**
+ * Add a fragment to the activity state. This fragment may optionally
+ * also have its view (if {@link Fragment#onCreateView Fragment.onCreateView}
+ * returns non-null) into a container view of the activity.
+ *
+ * @param containerViewId Optional identifier of the container this fragment is
+ * to be placed in. If 0, it will not be placed in a container.
+ * @param fragment The fragment to be added. This fragment must not already
+ * be added to the activity.
+ * @param tag Optional tag name for the fragment, to later retrieve the
+ * fragment with {@link Activity#findFragmentByTag(String)
+ * Activity.findFragmentByTag(String)}.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public FragmentTransaction add(int containerViewId, Fragment fragment, String tag);
+
+ /**
+ * Calls {@link #replace(int, Fragment, String)} with a null tag.
+ */
+ public FragmentTransaction replace(int containerViewId, Fragment fragment);
+
+ /**
+ * Replace an existing fragment that was added to a container. This is
+ * essentially the same as calling {@link #remove(Fragment)} for all
+ * currently added fragments that were added with the same containerViewId
+ * and then {@link #add(int, Fragment, String)} with the same arguments
+ * given here.
+ *
+ * @param containerViewId Identifier of the container whose fragment(s) are
+ * to be replaced.
+ * @param fragment The new fragment to place in the container.
+ * @param tag Optional tag name for the fragment, to later retrieve the
+ * fragment with {@link Activity#findFragmentByTag(String)
+ * Activity.findFragmentByTag(String)}.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag);
+
+ /**
+ * Remove an existing fragment. If it was added to a container, its view
+ * is also removed from that container.
+ *
+ * @param fragment The fragment to be removed.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public FragmentTransaction remove(Fragment fragment);
+
+ /**
+ * Hides an existing fragment. This is only relevant for fragments whose
+ * views have been added to a container, as this will cause the view to
+ * be hidden.
+ *
+ * @param fragment The fragment to be hidden.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public FragmentTransaction hide(Fragment fragment);
+
+ /**
+ * Hides a previously hidden fragment. This is only relevant for fragments whose
+ * views have been added to a container, as this will cause the view to
+ * be shown.
+ *
+ * @param fragment The fragment to be shown.
+ *
+ * @return Returns the same FragmentTransaction instance.
+ */
+ public FragmentTransaction show(Fragment fragment);
+
+ /**
+ * Bit mask that is set for all enter transitions.
+ */
+ public final int TRANSIT_ENTER_MASK = 0x1000;
+
+ /**
+ * Bit mask that is set for all exit transitions.
+ */
+ public final int TRANSIT_EXIT_MASK = 0x2000;
+
+ /** Not set up for a transition. */
+ public final int TRANSIT_UNSET = -1;
+ /** No animation for transition. */
+ public final int TRANSIT_NONE = 0;
+ /** Window has been added to the screen. */
+ public final int TRANSIT_ENTER = 1 | TRANSIT_ENTER_MASK;
+ /** Window has been removed from the screen. */
+ public final int TRANSIT_EXIT = 2 | TRANSIT_EXIT_MASK;
+ /** Window has been made visible. */
+ public final int TRANSIT_SHOW = 3 | TRANSIT_ENTER_MASK;
+ /** Window has been made invisible. */
+ public final int TRANSIT_HIDE = 4 | TRANSIT_EXIT_MASK;
+ /** The "application starting" preview window is no longer needed, and will
+ * animate away to show the real window. */
+ public final int TRANSIT_PREVIEW_DONE = 5;
+ /** A window in a new activity is being opened on top of an existing one
+ * in the same task. */
+ public final int TRANSIT_ACTIVITY_OPEN = 6 | TRANSIT_ENTER_MASK;
+ /** The window in the top-most activity is being closed to reveal the
+ * previous activity in the same task. */
+ public final int TRANSIT_ACTIVITY_CLOSE = 7 | TRANSIT_EXIT_MASK;
+ /** A window in a new task is being opened on top of an existing one
+ * in another activity's task. */
+ public final int TRANSIT_TASK_OPEN = 8 | TRANSIT_ENTER_MASK;
+ /** A window in the top-most activity is being closed to reveal the
+ * previous activity in a different task. */
+ public final int TRANSIT_TASK_CLOSE = 9 | TRANSIT_EXIT_MASK;
+ /** A window in an existing task is being displayed on top of an existing one
+ * in another activity's task. */
+ public final int TRANSIT_TASK_TO_FRONT = 10 | TRANSIT_ENTER_MASK;
+ /** A window in an existing task is being put below all other tasks. */
+ public final int TRANSIT_TASK_TO_BACK = 11 | TRANSIT_EXIT_MASK;
+ /** A window in a new activity that doesn't have a wallpaper is being
+ * opened on top of one that does, effectively closing the wallpaper. */
+ public final int TRANSIT_WALLPAPER_CLOSE = 12 | TRANSIT_EXIT_MASK;
+ /** A window in a new activity that does have a wallpaper is being
+ * opened on one that didn't, effectively opening the wallpaper. */
+ public final int TRANSIT_WALLPAPER_OPEN = 13 | TRANSIT_ENTER_MASK;
+ /** A window in a new activity is being opened on top of an existing one,
+ * and both are on top of the wallpaper. */
+ public final int TRANSIT_WALLPAPER_INTRA_OPEN = 14 | TRANSIT_ENTER_MASK;
+ /** The window in the top-most activity is being closed to reveal the
+ * previous activity, and both are on top of he wallpaper. */
+ public final int TRANSIT_WALLPAPER_INTRA_CLOSE = 15 | TRANSIT_EXIT_MASK;
+
+ public FragmentTransaction setCustomAnimations(int enter, int exit);
+
+ public FragmentTransaction setTransition(int transit);
+ public FragmentTransaction setTransitionStyle(int styleRes);
+
+ public FragmentTransaction addToBackStack(String name);
+ public void commit();
+}
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 20c9a80..8ea59a7 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -316,7 +316,11 @@ public interface IActivityManager extends IInterface {
public void crashApplication(int uid, int initialPid, String packageName,
String message) throws RemoteException;
-
+
+ // Cause the specified process to dump the specified heap.
+ public boolean dumpHeap(String process, boolean managed, String path,
+ ParcelFileDescriptor fd) throws RemoteException;
+
/*
* Private non-Binder interfaces
*/
@@ -533,4 +537,5 @@ public interface IActivityManager extends IInterface {
int SET_IMMERSIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+111;
int IS_TOP_ACTIVITY_IMMERSIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+112;
int CRASH_APPLICATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+113;
+ int DUMP_HEAP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+114;
}
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
index c8ef17f..039bcb9 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -97,6 +97,8 @@ public interface IApplicationThread extends IInterface {
void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException;
void profilerControl(boolean start, String path, ParcelFileDescriptor fd)
throws RemoteException;
+ void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd)
+ throws RemoteException;
void setSchedulingGroup(int group) throws RemoteException;
void getMemoryInfo(Debug.MemoryInfo outInfo) throws RemoteException;
static final int PACKAGE_REMOVED = 0;
@@ -140,4 +142,5 @@ public interface IApplicationThread extends IInterface {
int SCHEDULE_SUICIDE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32;
int DISPATCH_PACKAGE_BROADCAST_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+33;
int SCHEDULE_CRASH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+34;
+ int DUMP_HEAP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+35;
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index b8c3aa3..4d5f36a 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -997,8 +997,10 @@ public class Instrumentation {
IllegalAccessException {
Activity activity = (Activity)clazz.newInstance();
ActivityThread aThread = null;
- activity.attach(context, aThread, this, token, application, intent, info, title,
- parent, id, lastNonConfigurationInstance, new Configuration());
+ activity.attach(context, aThread, this, token, application, intent,
+ info, title, parent, id,
+ (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
+ new Configuration());
return activity;
}
@@ -1058,21 +1060,23 @@ public class Instrumentation {
}
public void callActivityOnDestroy(Activity activity) {
- if (mWaitingActivities != null) {
- synchronized (mSync) {
- final int N = mWaitingActivities.size();
- for (int i=0; i<N; i++) {
- final ActivityWaiter aw = mWaitingActivities.get(i);
- final Intent intent = aw.intent;
- if (intent.filterEquals(activity.getIntent())) {
- aw.activity = activity;
- mMessageQueue.addIdleHandler(new ActivityGoing(aw));
- }
- }
- }
- }
+ // TODO: the following block causes intermittent hangs when using startActivity
+ // temporarily comment out until root cause is fixed (bug 2630683)
+// if (mWaitingActivities != null) {
+// synchronized (mSync) {
+// final int N = mWaitingActivities.size();
+// for (int i=0; i<N; i++) {
+// final ActivityWaiter aw = mWaitingActivities.get(i);
+// final Intent intent = aw.intent;
+// if (intent.filterEquals(activity.getIntent())) {
+// aw.activity = activity;
+// mMessageQueue.addIdleHandler(new ActivityGoing(aw));
+// }
+// }
+// }
+// }
- activity.onDestroy();
+ activity.performDestroy();
if (mActivityMonitors != null) {
synchronized (mSync) {
@@ -1331,7 +1335,7 @@ public class Instrumentation {
* is being started.
* @param token Internal token identifying to the system who is starting
* the activity; may be null.
- * @param target Which activity is perform the start (and thus receiving
+ * @param target Which activity is performing the start (and thus receiving
* any result); may be null if this call is not being made
* from an activity.
* @param intent The actual Intent to start.
@@ -1381,6 +1385,64 @@ public class Instrumentation {
return null;
}
+ /**
+ * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)},
+ * but for calls from a {#link Fragment}.
+ *
+ * @param who The Context from which the activity is being started.
+ * @param contextThread The main thread of the Context from which the activity
+ * is being started.
+ * @param token Internal token identifying to the system who is starting
+ * the activity; may be null.
+ * @param target Which fragment is performing the start (and thus receiving
+ * any result).
+ * @param intent The actual Intent to start.
+ * @param requestCode Identifier for this request's result; less than zero
+ * if the caller is not expecting a result.
+ *
+ * @return To force the return of a particular result, return an
+ * ActivityResult object containing the desired data; otherwise
+ * return null. The default implementation always returns null.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Activity#startActivity(Intent)
+ * @see Activity#startActivityForResult(Intent, int)
+ * @see Activity#startActivityFromChild
+ *
+ * {@hide}
+ */
+ public ActivityResult execStartActivity(
+ Context who, IBinder contextThread, IBinder token, Fragment target,
+ Intent intent, int requestCode) {
+ IApplicationThread whoThread = (IApplicationThread) contextThread;
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return requestCode >= 0 ? am.getResult() : null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ int result = ActivityManagerNative.getDefault()
+ .startActivity(whoThread, intent,
+ intent.resolveTypeIfNeeded(who.getContentResolver()),
+ null, 0, token, target != null ? target.mWho : null,
+ requestCode, false, false);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
/*package*/ final void init(ActivityThread thread,
Context instrContext, Context appContext, ComponentName component,
IInstrumentationWatcher watcher) {
diff --git a/core/java/android/app/ListActivity.java b/core/java/android/app/ListActivity.java
index 4bf5518..d49968f 100644
--- a/core/java/android/app/ListActivity.java
+++ b/core/java/android/app/ListActivity.java
@@ -309,7 +309,7 @@ public class ListActivity extends Activity {
if (mList != null) {
return;
}
- setContentView(com.android.internal.R.layout.list_content);
+ setContentView(com.android.internal.R.layout.list_content_simple);
}
diff --git a/core/java/android/app/ListFragment.java b/core/java/android/app/ListFragment.java
new file mode 100644
index 0000000..73ef869
--- /dev/null
+++ b/core/java/android/app/ListFragment.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * An fragment that displays a list of items by binding to a data source such as
+ * an array or Cursor, and exposes event handlers when the user selects an item.
+ * <p>
+ * ListActivity hosts a {@link android.widget.ListView ListView} object that can
+ * be bound to different data sources, typically either an array or a Cursor
+ * holding query results. Binding, screen layout, and row layout are discussed
+ * in the following sections.
+ * <p>
+ * <strong>Screen Layout</strong>
+ * </p>
+ * <p>
+ * ListActivity has a default layout that consists of a single list view.
+ * However, if you desire, you can customize the fragment layout by returning
+ * your own view hierarchy from {@link #onCreateView}.
+ * To do this, your view hierarchy MUST contain a ListView object with the
+ * id "@android:id/list" (or {@link android.R.id#list} if it's in code)
+ * <p>
+ * Optionally, your view hierarchy can contain another view object of any type to
+ * display when the list view is empty. This "empty list" notifier must have an
+ * id "android:empty". Note that when an empty view is present, the list view
+ * will be hidden when there is no data to display.
+ * <p>
+ * The following code demonstrates an (ugly) custom lisy layout. It has a list
+ * with a green background, and an alternate red "no data" message.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:orientation=&quot;vertical&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:paddingLeft=&quot;8dp&quot;
+ * android:paddingRight=&quot;8dp&quot;&gt;
+ *
+ * &lt;ListView android:id=&quot;@id/android:list&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#00FF00&quot;
+ * android:layout_weight=&quot;1&quot;
+ * android:drawSelectorOnTop=&quot;false&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@id/android:empty&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;match_parent&quot;
+ * android:background=&quot;#FF0000&quot;
+ * android:text=&quot;No data&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * <strong>Row Layout</strong>
+ * </p>
+ * <p>
+ * You can specify the layout of individual rows in the list. You do this by
+ * specifying a layout resource in the ListAdapter object hosted by the fragment
+ * (the ListAdapter binds the ListView to the data; more on this later).
+ * <p>
+ * A ListAdapter constructor takes a parameter that specifies a layout resource
+ * for each row. It also has two additional parameters that let you specify
+ * which data field to associate with which object in the row layout resource.
+ * These two parameters are typically parallel arrays.
+ * </p>
+ * <p>
+ * Android provides some standard row layout resources. These are in the
+ * {@link android.R.layout} class, and have names such as simple_list_item_1,
+ * simple_list_item_2, and two_line_list_item. The following layout XML is the
+ * source for the resource two_line_list_item, which displays two data
+ * fields,one above the other, for each list row.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;
+ * android:orientation=&quot;vertical&quot;&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text1&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:textStyle=&quot;bold&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;TextView android:id=&quot;@+id/text2&quot;
+ * android:textSize=&quot;16sp&quot;
+ * android:layout_width=&quot;match_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * You must identify the data bound to each TextView object in this layout. The
+ * syntax for this is discussed in the next section.
+ * </p>
+ * <p>
+ * <strong>Binding to Data</strong>
+ * </p>
+ * <p>
+ * You bind the ListFragment's ListView object to data using a class that
+ * implements the {@link android.widget.ListAdapter ListAdapter} interface.
+ * Android provides two standard list adapters:
+ * {@link android.widget.SimpleAdapter SimpleAdapter} for static data (Maps),
+ * and {@link android.widget.SimpleCursorAdapter SimpleCursorAdapter} for Cursor
+ * query results.
+ * </p>
+ *
+ * @see #setListAdapter
+ * @see android.widget.ListView
+ */
+public class ListFragment extends Fragment {
+ final private Handler mHandler = new Handler();
+
+ final private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mList.focusableViewAvailable(mList);
+ }
+ };
+
+ final private AdapterView.OnItemClickListener mOnClickListener
+ = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ onListItemClick((ListView)parent, v, position, id);
+ }
+ };
+
+ ListAdapter mAdapter;
+ ListView mList;
+ View mEmptyView;
+ TextView mStandardEmptyView;
+ View mProgressContainer;
+ View mListContainer;
+ boolean mListShown;
+
+ public ListFragment() {
+ }
+
+ /**
+ * Provide default implementation to return a simple list view. Subclasses
+ * can override to replace with their own layout. If doing so, the
+ * returned view hierarchy <em>must</em> have a ListView whose id
+ * is {@link android.R.id#list android.R.id.list} and can optionally
+ * have a sibling view id {@link android.R.id#empty android.R.id.empty}
+ * that is to be shown when the list is empty.
+ *
+ * <p>If you are overriding this method with your own custom content,
+ * consider including the standard layout {@link android.R.layout#list_content}
+ * in your layout file, so that you continue to retain all of the standard
+ * behavior of ListFragment. In particular, this is currently the only
+ * way to have the built-in indeterminant progress state be shown.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(com.android.internal.R.layout.list_content,
+ container, false);
+ }
+
+ /**
+ * Attach to list view once Fragment is ready to run.
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ ensureList();
+ }
+
+ /**
+ * Detach from list view.
+ */
+ @Override
+ public void onDestroyView() {
+ mHandler.removeCallbacks(mRequestFocus);
+ mList = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * This method will be called when an item in the list is selected.
+ * Subclasses should override. Subclasses can call
+ * getListView().getItemAtPosition(position) if they need to access the
+ * data associated with the selected item.
+ *
+ * @param l The ListView where the click happened
+ * @param v The view that was clicked within the ListView
+ * @param position The position of the view in the list
+ * @param id The row id of the item that was clicked
+ */
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ }
+
+ /**
+ * Provide the cursor for the list view.
+ */
+ public void setListAdapter(ListAdapter adapter) {
+ boolean hadAdapter = mAdapter != null;
+ mAdapter = adapter;
+ if (mList != null) {
+ mList.setAdapter(adapter);
+ if (!mListShown && !hadAdapter) {
+ // The list was hidden, and previously didn't have an
+ // adapter. It is now time to show it.
+ setListShown(true, getView().getWindowToken() != null);
+ }
+ }
+ }
+
+ /**
+ * Set the currently selected list item to the specified
+ * position with the adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ ensureList();
+ mList.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected list item.
+ */
+ public int getSelectedItemPosition() {
+ ensureList();
+ return mList.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected list item.
+ */
+ public long getSelectedItemId() {
+ ensureList();
+ return mList.getSelectedItemId();
+ }
+
+ /**
+ * Get the activity's list view widget.
+ */
+ public ListView getListView() {
+ ensureList();
+ return mList;
+ }
+
+ /**
+ * The default content for a ListFragment has a TextView that can
+ * be shown when the list is empty. If you would like to have it
+ * shown, call this method to supply the text it should use.
+ */
+ public void setEmptyText(CharSequence text) {
+ ensureList();
+ if (mStandardEmptyView == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ mList.setEmptyView(mStandardEmptyView);
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not
+ * displayed if you are waiting for the initial data to show in it. During
+ * this time an indeterminant progress indicator will be shown instead.
+ *
+ * <p>Applications do not normally need to use this themselves. The default
+ * behavior of ListFragment is to start with the list not being shown, only
+ * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}.
+ * If the list at that point had not been shown, when it does get shown
+ * it will be do without the user ever seeing the hidden state.
+ *
+ * @param shown If true, the list view is shown; if false, the progress
+ * indicator. The initial value is true.
+ */
+ public void setListShown(boolean shown) {
+ setListShown(shown, true);
+ }
+
+ /**
+ * Like {@link #setListShown(boolean)}, but no animation is used when
+ * transitioning from the previous state.
+ */
+ public void setListShownNoAnimation(boolean shown) {
+ setListShown(shown, false);
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not
+ * displayed if you are waiting for the initial data to show in it. During
+ * this time an indeterminant progress indicator will be shown instead.
+ *
+ * @param shown If true, the list view is shown; if false, the progress
+ * indicator. The initial value is true.
+ * @param animate If true, an animation will be used to transition to the
+ * new state.
+ */
+ private void setListShown(boolean shown, boolean animate) {
+ ensureList();
+ if (mProgressContainer == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ if (mListShown == shown) {
+ return;
+ }
+ mListShown = shown;
+ if (shown) {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_out));
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_in));
+ }
+ mProgressContainer.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.VISIBLE);
+ } else {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_in));
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_out));
+ }
+ mProgressContainer.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Get the ListAdapter associated with this activity's ListView.
+ */
+ public ListAdapter getListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ View root = getView();
+ if (root == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+ if (root instanceof ListView) {
+ mList = (ListView)root;
+ } else {
+ mStandardEmptyView = (TextView)root.findViewById(
+ com.android.internal.R.id.internalEmpty);
+ if (mStandardEmptyView == null) {
+ mEmptyView = root.findViewById(android.R.id.empty);
+ }
+ mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer);
+ mListContainer = root.findViewById(com.android.internal.R.id.listContainer);
+ View rawListView = root.findViewById(android.R.id.list);
+ if (!(rawListView instanceof ListView)) {
+ throw new RuntimeException(
+ "Content has view with id attribute 'android.R.id.list' "
+ + "that is not a ListView class");
+ }
+ mList = (ListView)rawListView;
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (mEmptyView != null) {
+ mList.setEmptyView(mEmptyView);
+ }
+ }
+ mListShown = true;
+ mList.setOnItemClickListener(mOnClickListener);
+ if (mAdapter != null) {
+ setListAdapter(mAdapter);
+ } else {
+ // We are starting without an adapter, so assume we won't
+ // have our data right away and start with the progress indicator.
+ if (mProgressContainer != null) {
+ setListShown(false, false);
+ }
+ }
+ mHandler.post(mRequestFocus);
+ }
+}
diff --git a/core/java/android/app/LoaderManager.java b/core/java/android/app/LoaderManager.java
new file mode 100644
index 0000000..7600899
--- /dev/null
+++ b/core/java/android/app/LoaderManager.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.os.Bundle;
+import android.util.SparseArray;
+
+/**
+ * Object associated with an {@link Activity} or {@link Fragment} for managing
+ * one or more {@link android.content.Loader} instances associated with it.
+ */
+public class LoaderManager {
+ final SparseArray<LoaderInfo> mLoaders = new SparseArray<LoaderInfo>();
+ final SparseArray<LoaderInfo> mInactiveLoaders = new SparseArray<LoaderInfo>();
+ boolean mStarted;
+ boolean mRetaining;
+ boolean mRetainingStarted;
+
+ /**
+ * Callback interface for a client to interact with the manager.
+ */
+ public interface LoaderCallbacks<D> {
+ public Loader<D> onCreateLoader(int id, Bundle args);
+ public void onLoadFinished(Loader<D> loader, D data);
+ }
+
+ final class LoaderInfo implements Loader.OnLoadCompleteListener<Object> {
+ final int mId;
+ final Bundle mArgs;
+ LoaderManager.LoaderCallbacks<Object> mCallbacks;
+ Loader<Object> mLoader;
+ Object mData;
+ boolean mStarted;
+ boolean mRetaining;
+ boolean mRetainingStarted;
+ boolean mDestroyed;
+ boolean mListenerRegistered;
+
+ public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks<Object> callbacks) {
+ mId = id;
+ mArgs = args;
+ mCallbacks = callbacks;
+ }
+
+ void start() {
+ if (mRetaining && mRetainingStarted) {
+ // Our owner is started, but we were being retained from a
+ // previous instance in the started state... so there is really
+ // nothing to do here, since the loaders are still started.
+ mStarted = true;
+ return;
+ }
+
+ if (mLoader == null && mCallbacks != null) {
+ mLoader = mCallbacks.onCreateLoader(mId, mArgs);
+ }
+ if (mLoader != null) {
+ mLoader.registerListener(mId, this);
+ mListenerRegistered = true;
+ mLoader.startLoading();
+ mStarted = true;
+ }
+ }
+
+ void retain() {
+ mRetaining = true;
+ mRetainingStarted = mStarted;
+ mStarted = false;
+ mCallbacks = null;
+ }
+
+ void finishRetain() {
+ if (mRetaining) {
+ mRetaining = false;
+ if (mStarted != mRetainingStarted) {
+ if (!mStarted) {
+ // This loader was retained in a started state, but
+ // at the end of retaining everything our owner is
+ // no longer started... so make it stop.
+ stop();
+ }
+ }
+ if (mStarted && mData != null && mCallbacks != null) {
+ // This loader was retained, and now at the point of
+ // finishing the retain we find we remain started, have
+ // our data, and the owner has a new callback... so
+ // let's deliver the data now.
+ mCallbacks.onLoadFinished(mLoader, mData);
+ }
+ }
+ }
+
+ void stop() {
+ mStarted = false;
+ if (mLoader != null && mListenerRegistered) {
+ // Let the loader know we're done with it
+ mListenerRegistered = false;
+ mLoader.unregisterListener(this);
+ }
+ }
+
+ void destroy() {
+ mDestroyed = true;
+ mCallbacks = null;
+ if (mLoader != null) {
+ if (mListenerRegistered) {
+ mListenerRegistered = false;
+ mLoader.unregisterListener(this);
+ }
+ mLoader.destroy();
+ }
+ }
+
+ @Override public void onLoadComplete(Loader<Object> loader, Object data) {
+ if (mDestroyed) {
+ return;
+ }
+
+ // Notify of the new data so the app can switch out the old data before
+ // we try to destroy it.
+ mData = data;
+ if (mCallbacks != null) {
+ mCallbacks.onLoadFinished(loader, data);
+ }
+
+ // Look for an inactive loader and destroy it if found
+ LoaderInfo info = mInactiveLoaders.get(mId);
+ if (info != null) {
+ Loader<Object> oldLoader = info.mLoader;
+ if (oldLoader != null) {
+ oldLoader.unregisterListener(info);
+ oldLoader.destroy();
+ }
+ mInactiveLoaders.remove(mId);
+ }
+ }
+ }
+
+ LoaderManager(boolean started) {
+ mStarted = started;
+ }
+
+ private LoaderInfo createLoader(int id, Bundle args,
+ LoaderManager.LoaderCallbacks<Object> callback) {
+ LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
+ mLoaders.put(id, info);
+ Loader<Object> loader = callback.onCreateLoader(id, args);
+ info.mLoader = (Loader<Object>)loader;
+ if (mStarted) {
+ // The activity will start all existing loaders in it's onStart(), so only start them
+ // here if we're past that point of the activitiy's life cycle
+ loader.registerListener(id, info);
+ loader.startLoading();
+ }
+ return info;
+ }
+
+ /**
+ * Ensures a loader is initialized an active. If the loader doesn't
+ * already exist, one is created and started. Otherwise the last created
+ * loader is re-used.
+ *
+ * <p>In either case, the given callback is associated with the loader, and
+ * will be called as the loader state changes. If at the point of call
+ * the caller is in its started state, and the requested loader
+ * already exists and has generated its data, then
+ * callback. {@link LoaderCallbacks#onLoadFinished} will
+ * be called immediately (inside of this function), so you must be prepared
+ * for this to happen.
+ */
+ @SuppressWarnings("unchecked")
+ public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ LoaderInfo info = mLoaders.get(id);
+
+ if (info == null) {
+ // Loader doesn't already exist; create.
+ info = createLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
+ } else {
+ info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
+ }
+
+ if (info.mData != null && mStarted) {
+ // If the loader has already generated its data, report it now.
+ info.mCallbacks.onLoadFinished(info.mLoader, info.mData);
+ }
+
+ return (Loader<D>)info.mLoader;
+ }
+
+ /**
+ * Create a new loader in this manager, registers the callbacks to it,
+ * and starts it loading. If a loader with the same id has previously been
+ * started it will automatically be destroyed when the new loader completes
+ * its work. The callback will be delivered before the old loader
+ * is destroyed.
+ */
+ @SuppressWarnings("unchecked")
+ public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ LoaderInfo info = mLoaders.get(id);
+ if (info != null) {
+ if (mInactiveLoaders.get(id) != null) {
+ // We already have an inactive loader for this ID that we are
+ // waiting for! Now we have three active loaders... let's just
+ // drop the one in the middle, since we are still waiting for
+ // its result but that result is already out of date.
+ info.destroy();
+ } else {
+ // Keep track of the previous instance of this loader so we can destroy
+ // it when the new one completes.
+ mInactiveLoaders.put(id, info);
+ }
+ }
+
+ info = createLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
+ return (Loader<D>)info.mLoader;
+ }
+
+ /**
+ * Stops and removes the loader with the given ID.
+ */
+ public void stopLoader(int id) {
+ int idx = mLoaders.indexOfKey(id);
+ if (idx >= 0) {
+ LoaderInfo info = mLoaders.valueAt(idx);
+ mLoaders.removeAt(idx);
+ Loader<Object> loader = info.mLoader;
+ if (loader != null) {
+ loader.unregisterListener(info);
+ loader.destroy();
+ }
+ }
+ }
+
+ /**
+ * Return the Loader with the given id or null if no matching Loader
+ * is found.
+ */
+ @SuppressWarnings("unchecked")
+ public <D> Loader<D> getLoader(int id) {
+ LoaderInfo loaderInfo = mLoaders.get(id);
+ if (loaderInfo != null) {
+ return (Loader<D>)mLoaders.get(id).mLoader;
+ }
+ return null;
+ }
+
+ void doStart() {
+ // Call out to sub classes so they can start their loaders
+ // Let the existing loaders know that we want to be notified when a load is complete
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).start();
+ }
+ mStarted = true;
+ }
+
+ void doStop() {
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).stop();
+ }
+ mStarted = false;
+ }
+
+ void doRetain() {
+ mRetaining = true;
+ mStarted = false;
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).retain();
+ }
+ }
+
+ void finishRetain() {
+ mRetaining = false;
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).finishRetain();
+ }
+ }
+
+ void doDestroy() {
+ if (!mRetaining) {
+ for (int i = mLoaders.size()-1; i >= 0; i--) {
+ mLoaders.valueAt(i).destroy();
+ }
+ }
+
+ for (int i = mInactiveLoaders.size()-1; i >= 0; i--) {
+ mInactiveLoaders.valueAt(i).destroy();
+ }
+ mInactiveLoaders.clear();
+ }
+}
diff --git a/core/java/android/app/LoaderManagingFragment.java b/core/java/android/app/LoaderManagingFragment.java
new file mode 100644
index 0000000..5d417a0
--- /dev/null
+++ b/core/java/android/app/LoaderManagingFragment.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Loader;
+import android.os.Bundle;
+
+import java.util.HashMap;
+
+/**
+ * A Fragment that has utility methods for managing {@link Loader}s.
+ *
+ * @param <D> The type of data returned by the Loader. If you're using multiple Loaders with
+ * different return types use Object and case the results.
+ */
+public abstract class LoaderManagingFragment<D> extends Fragment
+ implements Loader.OnLoadCompleteListener<D> {
+ private boolean mStarted = false;
+
+ static final class LoaderInfo<D> {
+ public Bundle args;
+ public Loader<D> loader;
+ }
+ private HashMap<Integer, LoaderInfo<D>> mLoaders;
+ private HashMap<Integer, LoaderInfo<D>> mInactiveLoaders;
+
+ /**
+ * Registers a loader with this activity, registers the callbacks on it, and starts it loading.
+ * If a loader with the same id has previously been started it will automatically be destroyed
+ * when the new loader completes it's work. The callback will be delivered before the old loader
+ * is destroyed.
+ */
+ public Loader<D> startLoading(int id, Bundle args) {
+ LoaderInfo<D> info = mLoaders.get(id);
+ if (info != null) {
+ // Keep track of the previous instance of this loader so we can destroy
+ // it when the new one completes.
+ mInactiveLoaders.put(id, info);
+ }
+ info = new LoaderInfo<D>();
+ info.args = args;
+ mLoaders.put(id, info);
+ Loader<D> loader = onCreateLoader(id, args);
+ info.loader = loader;
+ if (mStarted) {
+ // The activity will start all existing loaders in it's onStart(), so only start them
+ // here if we're past that point of the activitiy's life cycle
+ loader.registerListener(id, this);
+ loader.startLoading();
+ }
+ return loader;
+ }
+
+ protected abstract Loader<D> onCreateLoader(int id, Bundle args);
+ protected abstract void onInitializeLoaders();
+ protected abstract void onLoadFinished(Loader<D> loader, D data);
+
+ public final void onLoadComplete(Loader<D> loader, D data) {
+ // Notify of the new data so the app can switch out the old data before
+ // we try to destroy it.
+ onLoadFinished(loader, data);
+
+ // Look for an inactive loader and destroy it if found
+ int id = loader.getId();
+ LoaderInfo<D> info = mInactiveLoaders.get(id);
+ if (info != null) {
+ Loader<D> oldLoader = info.loader;
+ if (oldLoader != null) {
+ oldLoader.destroy();
+ }
+ mInactiveLoaders.remove(id);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ if (mLoaders == null) {
+ // Look for a passed along loader and create a new one if it's not there
+// TODO: uncomment once getLastNonConfigurationInstance method is available
+// mLoaders = (HashMap<Integer, LoaderInfo>) getLastNonConfigurationInstance();
+ if (mLoaders == null) {
+ mLoaders = new HashMap<Integer, LoaderInfo<D>>();
+ onInitializeLoaders();
+ }
+ }
+ if (mInactiveLoaders == null) {
+ mInactiveLoaders = new HashMap<Integer, LoaderInfo<D>>();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // Call out to sub classes so they can start their loaders
+ // Let the existing loaders know that we want to be notified when a load is complete
+ for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+ LoaderInfo<D> info = entry.getValue();
+ Loader<D> loader = info.loader;
+ int id = entry.getKey();
+ if (loader == null) {
+ loader = onCreateLoader(id, info.args);
+ info.loader = loader;
+ }
+ loader.registerListener(id, this);
+ loader.startLoading();
+ }
+
+ mStarted = true;
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+ LoaderInfo<D> info = entry.getValue();
+ Loader<D> loader = info.loader;
+ if (loader == null) {
+ continue;
+ }
+
+ // Let the loader know we're done with it
+ loader.unregisterListener(this);
+
+ // The loader isn't getting passed along to the next instance so ask it to stop loading
+ if (!getActivity().isChangingConfigurations()) {
+ loader.stopLoading();
+ }
+ }
+
+ mStarted = false;
+ }
+
+ /** TO DO: This needs to be turned into a retained fragment.
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ // Pass the loader along to the next guy
+ Object result = mLoaders;
+ mLoaders = null;
+ return result;
+ }
+ **/
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (mLoaders != null) {
+ for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+ LoaderInfo<D> info = entry.getValue();
+ Loader<D> loader = info.loader;
+ if (loader == null) {
+ continue;
+ }
+ loader.destroy();
+ }
+ }
+ }
+
+ /**
+ * Stops and removes the loader with the given ID.
+ */
+ public void stopLoading(int id) {
+ if (mLoaders != null) {
+ LoaderInfo<D> info = mLoaders.remove(id);
+ if (info != null) {
+ Loader<D> loader = info.loader;
+ if (loader != null) {
+ loader.unregisterListener(this);
+ loader.destroy();
+ }
+ }
+ }
+ }
+
+ /**
+ * @return the Loader with the given id or null if no matching Loader
+ * is found.
+ */
+ public Loader<D> getLoader(int id) {
+ LoaderInfo<D> loaderInfo = mLoaders.get(id);
+ if (loaderInfo != null) {
+ return mLoaders.get(id).loader;
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java
index a24fcae..524de6f 100644
--- a/core/java/android/app/LocalActivityManager.java
+++ b/core/java/android/app/LocalActivityManager.java
@@ -20,13 +20,11 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Binder;
import android.os.Bundle;
-import android.util.Config;
import android.util.Log;
import android.view.Window;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.Map;
/**
@@ -38,7 +36,7 @@ import java.util.Map;
*/
public class LocalActivityManager {
private static final String TAG = "LocalActivityManager";
- private static final boolean localLOGV = false || Config.LOGV;
+ private static final boolean localLOGV = false;
// Internal token for an Activity being managed by LocalActivityManager.
private static class LocalActivityRecord extends Binder {
@@ -112,11 +110,16 @@ public class LocalActivityManager {
if (r.curState == INITIALIZING) {
// Get the lastNonConfigurationInstance for the activity
- HashMap<String,Object> lastNonConfigurationInstances =
- mParent.getLastNonConfigurationChildInstances();
- Object instance = null;
+ HashMap<String, Object> lastNonConfigurationInstances =
+ mParent.getLastNonConfigurationChildInstances();
+ Object instanceObj = null;
if (lastNonConfigurationInstances != null) {
- instance = lastNonConfigurationInstances.get(r.id);
+ instanceObj = lastNonConfigurationInstances.get(r.id);
+ }
+ Activity.NonConfigurationInstances instance = null;
+ if (instanceObj != null) {
+ instance = new Activity.NonConfigurationInstances();
+ instance.activity = instanceObj;
}
// We need to have always created the activity.
@@ -346,7 +349,7 @@ public class LocalActivityManager {
}
private Window performDestroy(LocalActivityRecord r, boolean finish) {
- Window win = null;
+ Window win;
win = r.window;
if (r.curState == RESUMED && !finish) {
performPause(r, finish);
@@ -380,7 +383,8 @@ public class LocalActivityManager {
if (r != null) {
win = performDestroy(r, finish);
if (finish) {
- mActivities.remove(r);
+ mActivities.remove(id);
+ mActivityArray.remove(r);
}
}
return win;
@@ -441,10 +445,8 @@ public class LocalActivityManager {
*/
public void dispatchCreate(Bundle state) {
if (state != null) {
- final Iterator<String> i = state.keySet().iterator();
- while (i.hasNext()) {
+ for (String id : state.keySet()) {
try {
- final String id = i.next();
final Bundle astate = state.getBundle(id);
LocalActivityRecord r = mActivities.get(id);
if (r != null) {
@@ -457,9 +459,7 @@ public class LocalActivityManager {
}
} catch (Exception e) {
// Recover from -all- app errors.
- Log.e(TAG,
- "Exception thrown when restoring LocalActivityManager state",
- e);
+ Log.e(TAG, "Exception thrown when restoring LocalActivityManager state", e);
}
}
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 296d70a4..3066f5c 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -46,7 +46,7 @@ public class DevicePolicyManager {
private final Context mContext;
private final IDevicePolicyManager mService;
-
+
private final Handler mHandler;
private DevicePolicyManager(Context context, Handler handler) {
@@ -61,14 +61,14 @@ public class DevicePolicyManager {
DevicePolicyManager me = new DevicePolicyManager(context, handler);
return me.mService != null ? me : null;
}
-
+
/**
* Activity action: ask the user to add a new device administrator to the system.
* The desired policy is the ComponentName of the policy in the
* {@link #EXTRA_DEVICE_ADMIN} extra field. This will invoke a UI to
* bring the user through adding the device administrator to the system (or
* allowing them to reject it).
- *
+ *
* <p>You can optionally include the {@link #EXTRA_ADD_EXPLANATION}
* field to provide the user with additional explanation (in addition
* to your component's description) about what is being added.
@@ -76,7 +76,7 @@ public class DevicePolicyManager {
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_ADD_DEVICE_ADMIN
= "android.app.action.ADD_DEVICE_ADMIN";
-
+
/**
* Activity action: send when any policy admin changes a policy.
* This is generally used to find out when a new policy is in effect.
@@ -92,7 +92,7 @@ public class DevicePolicyManager {
* @see #ACTION_ADD_DEVICE_ADMIN
*/
public static final String EXTRA_DEVICE_ADMIN = "android.app.extra.DEVICE_ADMIN";
-
+
/**
* An optional CharSequence providing additional explanation for why the
* admin is being added.
@@ -100,22 +100,21 @@ public class DevicePolicyManager {
* @see #ACTION_ADD_DEVICE_ADMIN
*/
public static final String EXTRA_ADD_EXPLANATION = "android.app.extra.ADD_EXPLANATION";
-
- /**
- * Activity action: have the user enter a new password. This activity
- * should be launched after using {@link #setPasswordQuality(ComponentName, int)}
- * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the
- * user enter a new password that meets the current requirements. You can
- * use {@link #isActivePasswordSufficient()} to determine whether you need
- * to have the user select a new password in order to meet the current
- * constraints. Upon being resumed from this activity,
- * you can check the new password characteristics to see if they are
- * sufficient.
+
+ /**
+ * Activity action: have the user enter a new password. This activity should
+ * be launched after using {@link #setPasswordQuality(ComponentName, int)},
+ * or {@link #setPasswordMinimumLength(ComponentName, int)} to have the user
+ * enter a new password that meets the current requirements. You can use
+ * {@link #isActivePasswordSufficient()} to determine whether you need to
+ * have the user select a new password in order to meet the current
+ * constraints. Upon being resumed from this activity, you can check the new
+ * password characteristics to see if they are sufficient.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_SET_NEW_PASSWORD
= "android.app.action.SET_NEW_PASSWORD";
-
+
/**
* Return true if the given administrator component is currently
* active (enabled) in the system.
@@ -130,7 +129,7 @@ public class DevicePolicyManager {
}
return false;
}
-
+
/**
* Return a list of all currently active device administrator's component
* names. Note that if there are no administrators than null may be
@@ -146,7 +145,7 @@ public class DevicePolicyManager {
}
return null;
}
-
+
/**
* @hide
*/
@@ -160,7 +159,7 @@ public class DevicePolicyManager {
}
return false;
}
-
+
/**
* Remove a current administration component. This can only be called
* by the application that owns the administration component; if you
@@ -176,28 +175,28 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* Constant for {@link #setPasswordQuality}: the policy has no requirements
* for the password. Note that quality constants are ordered so that higher
* values are more restrictive.
*/
public static final int PASSWORD_QUALITY_UNSPECIFIED = 0;
-
+
/**
* Constant for {@link #setPasswordQuality}: the policy requires some kind
* of password, but doesn't care what it is. Note that quality constants
* are ordered so that higher values are more restrictive.
*/
public static final int PASSWORD_QUALITY_SOMETHING = 0x10000;
-
+
/**
* Constant for {@link #setPasswordQuality}: the user must have entered a
* password containing at least numeric characters. Note that quality
* constants are ordered so that higher values are more restrictive.
*/
public static final int PASSWORD_QUALITY_NUMERIC = 0x20000;
-
+
/**
* Constant for {@link #setPasswordQuality}: the user must have entered a
* password containing at least alphabetic (or other symbol) characters.
@@ -205,7 +204,7 @@ public class DevicePolicyManager {
* restrictive.
*/
public static final int PASSWORD_QUALITY_ALPHABETIC = 0x40000;
-
+
/**
* Constant for {@link #setPasswordQuality}: the user must have entered a
* password containing at least <em>both></em> numeric <em>and</em>
@@ -213,7 +212,19 @@ public class DevicePolicyManager {
* ordered so that higher values are more restrictive.
*/
public static final int PASSWORD_QUALITY_ALPHANUMERIC = 0x50000;
-
+
+ /**
+ * Constant for {@link #setPasswordQuality}: the user must have entered a
+ * password containing at least a letter, a numerical digit and a special
+ * symbol, by default. With this password quality, passwords can be
+ * restricted to contain various sets of characters, like at least an
+ * uppercase letter, etc. These are specified using various methods,
+ * like {@link #setPasswordMinimumLowerCase(ComponentName, int)}. Note
+ * that quality constants are ordered so that higher values are more
+ * restrictive.
+ */
+ public static final int PASSWORD_QUALITY_COMPLEX = 0x60000;
+
/**
* Called by an application that is administering the device to set the
* password restrictions it is imposing. After setting this, the user
@@ -222,21 +233,21 @@ public class DevicePolicyManager {
* will remain until the user has set a new one, so the change does not
* take place immediately. To prompt the user for a new password, use
* {@link #ACTION_SET_NEW_PASSWORD} after setting this value.
- *
+ *
* <p>Quality constants are ordered so that higher values are more restrictive;
* thus the highest requested quality constant (between the policy set here,
* the user's preference, and any other considerations) is the one that
* is in effect.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
* this method; if it has not, a security exception will be thrown.
- *
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param quality The new desired quality. One of
* {@link #PASSWORD_QUALITY_UNSPECIFIED}, {@link #PASSWORD_QUALITY_SOMETHING},
* {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC},
- * or {@link #PASSWORD_QUALITY_ALPHANUMERIC}.
+ * {@link #PASSWORD_QUALITY_ALPHANUMERIC} or {@link #PASSWORD_QUALITY_COMPLEX}.
*/
public void setPasswordQuality(ComponentName admin, int quality) {
if (mService != null) {
@@ -247,7 +258,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* Retrieve the current minimum password quality for all admins
* or a particular one.
@@ -264,7 +275,7 @@ public class DevicePolicyManager {
}
return PASSWORD_QUALITY_UNSPECIFIED;
}
-
+
/**
* Called by an application that is administering the device to set the
* minimum allowed password length. After setting this, the user
@@ -274,14 +285,14 @@ public class DevicePolicyManager {
* take place immediately. To prompt the user for a new password, use
* {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This
* constraint is only imposed if the administrator has also requested either
- * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC},
- * or {@link #PASSWORD_QUALITY_ALPHANUMERIC}
+ * {@link #PASSWORD_QUALITY_NUMERIC}, {@link #PASSWORD_QUALITY_ALPHABETIC}
+ * {@link #PASSWORD_QUALITY_ALPHANUMERIC}, or {@link #PASSWORD_QUALITY_COMPLEX}
* with {@link #setPasswordQuality}.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
* this method; if it has not, a security exception will be thrown.
- *
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param length The new desired minimum password length. A value of 0
* means there is no restriction.
@@ -295,7 +306,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* Retrieve the current minimum password length for all admins
* or a particular one.
@@ -312,7 +323,379 @@ public class DevicePolicyManager {
}
return 0;
}
-
+
+ /**
+ * Called by an application that is administering the device to set the
+ * minimum number of upper case letters required in the password. After
+ * setting this, the user will not be able to enter a new password that is
+ * not at least as restrictive as what has been set. Note that the current
+ * password will remain until the user has set a new one, so the change does
+ * not take place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This
+ * constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+ * default value is 0.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired minimum number of upper case letters
+ * required in the password. A value of 0 means there is no
+ * restriction.
+ */
+ public void setPasswordMinimumUpperCase(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumUpperCase(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of upper case letters required in the
+ * password for all admins or a particular one. This is the same value as
+ * set by {#link {@link #setPasswordMinimumUpperCase(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * @param admin The name of the admin component to check, or null to
+ * aggregate all admins.
+ * @return The minimum number of upper case letters required in the
+ * password.
+ */
+ public int getPasswordMinimumUpperCase(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumUpperCase(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the
+ * minimum number of lower case letters required in the password. After
+ * setting this, the user will not be able to enter a new password that is
+ * not at least as restrictive as what has been set. Note that the current
+ * password will remain until the user has set a new one, so the change does
+ * not take place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This
+ * constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+ * default value is 0.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired minimum number of lower case letters
+ * required in the password. A value of 0 means there is no
+ * restriction.
+ */
+ public void setPasswordMinimumLowerCase(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumLowerCase(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of lower case letters required in the
+ * password for all admins or a particular one. This is the same value as
+ * set by {#link {@link #setPasswordMinimumLowerCase(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * @param admin The name of the admin component to check, or null to
+ * aggregate all admins.
+ * @return The minimum number of lower case letters required in the
+ * password.
+ */
+ public int getPasswordMinimumLowerCase(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumLowerCase(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the
+ * minimum number of letters required in the password. After setting this,
+ * the user will not be able to enter a new password that is not at least as
+ * restrictive as what has been set. Note that the current password will
+ * remain until the user has set a new one, so the change does not take
+ * place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This
+ * constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+ * default value is 1.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired minimum number of letters required in the
+ * password. A value of 0 means there is no restriction.
+ */
+ public void setPasswordMinimumLetters(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumLetters(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of letters required in the password for all
+ * admins or a particular one. This is the same value as
+ * set by {#link {@link #setPasswordMinimumLetters(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * @param admin The name of the admin component to check, or null to
+ * aggregate all admins.
+ * @return The minimum number of letters required in the password.
+ */
+ public int getPasswordMinimumLetters(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumLetters(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the
+ * minimum number of numerical digits required in the password. After
+ * setting this, the user will not be able to enter a new password that is
+ * not at least as restrictive as what has been set. Note that the current
+ * password will remain until the user has set a new one, so the change does
+ * not take place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This
+ * constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+ * default value is 1.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired minimum number of numerical digits required
+ * in the password. A value of 0 means there is no restriction.
+ */
+ public void setPasswordMinimumNumeric(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumNumeric(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of numerical digits required in the password
+ * for all admins or a particular one. This is the same value as
+ * set by {#link {@link #setPasswordMinimumNumeric(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * @param admin The name of the admin component to check, or null to
+ * aggregate all admins.
+ * @return The minimum number of numerical digits required in the password.
+ */
+ public int getPasswordMinimumNumeric(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumNumeric(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the
+ * minimum number of symbols required in the password. After setting this,
+ * the user will not be able to enter a new password that is not at least as
+ * restrictive as what has been set. Note that the current password will
+ * remain until the user has set a new one, so the change does not take
+ * place immediately. To prompt the user for a new password, use
+ * {@link #ACTION_SET_NEW_PASSWORD} after setting this value. This
+ * constraint is only imposed if the administrator has also requested
+ * {@link #PASSWORD_QUALITY_COMPLEX} with {@link #setPasswordQuality}. The
+ * default value is 1.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired minimum number of symbols required in the
+ * password. A value of 0 means there is no restriction.
+ */
+ public void setPasswordMinimumSymbols(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumSymbols(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of symbols required in the password for all
+ * admins or a particular one. This is the same value as
+ * set by {#link {@link #setPasswordMinimumSymbols(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * @param admin The name of the admin component to check, or null to
+ * aggregate all admins.
+ * @return The minimum number of symbols required in the password.
+ */
+ public int getPasswordMinimumSymbols(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumSymbols(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the
+ * minimum number of non-letter characters (numerical digits or symbols)
+ * required in the password. After setting this, the user will not be able
+ * to enter a new password that is not at least as restrictive as what has
+ * been set. Note that the current password will remain until the user has
+ * set a new one, so the change does not take place immediately. To prompt
+ * the user for a new password, use {@link #ACTION_SET_NEW_PASSWORD} after
+ * setting this value. This constraint is only imposed if the administrator
+ * has also requested {@link #PASSWORD_QUALITY_COMPLEX} with
+ * {@link #setPasswordQuality}. The default value is 0.
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
+ * this method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired minimum number of letters required in the
+ * password. A value of 0 means there is no restriction.
+ */
+ public void setPasswordMinimumNonLetter(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordMinimumNonLetter(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current number of non-letter characters required in the
+ * password for all admins or a particular one. This is the same value as
+ * set by {#link {@link #setPasswordMinimumNonLetter(ComponentName, int)}
+ * and only applies when the password quality is
+ * {@link #PASSWORD_QUALITY_COMPLEX}.
+ *
+ * @param admin The name of the admin component to check, or null to
+ * aggregate all admins.
+ * @return The minimum number of letters required in the password.
+ */
+ public int getPasswordMinimumNonLetter(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordMinimumNonLetter(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called by an application that is administering the device to set the length
+ * of the password history. After setting this, the user will not be able to
+ * enter a new password that is the same as any password in the history. Note
+ * that the current password will remain until the user has set a new one, so
+ * the change does not take place immediately. To prompt the user for a new
+ * password, use {@link #ACTION_SET_NEW_PASSWORD} after setting this value.
+ * This constraint is only imposed if the administrator has also requested
+ * either {@link #PASSWORD_QUALITY_NUMERIC},
+ * {@link #PASSWORD_QUALITY_ALPHABETIC}, or
+ * {@link #PASSWORD_QUALITY_ALPHANUMERIC} with {@link #setPasswordQuality}.
+ *
+ * <p>
+ * The calling device admin must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this
+ * method; if it has not, a security exception will be thrown.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated
+ * with.
+ * @param length The new desired length of password history. A value of 0
+ * means there is no restriction.
+ */
+ public void setPasswordHistoryLength(ComponentName admin, int length) {
+ if (mService != null) {
+ try {
+ mService.setPasswordHistoryLength(admin, length);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current password history length for all admins
+ * or a particular one.
+ * @param admin The name of the admin component to check, or null to aggregate
+ * all admins.
+ * @return The length of the password history
+ */
+ public int getPasswordHistoryLength(ComponentName admin) {
+ if (mService != null) {
+ try {
+ return mService.getPasswordHistoryLength(admin);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed talking with device policy service", e);
+ }
+ }
+ return 0;
+ }
+
/**
* Return the maximum password length that the device supports for a
* particular password quality.
@@ -323,16 +706,16 @@ public class DevicePolicyManager {
// Kind-of arbitrary.
return 16;
}
-
+
/**
* Determine whether the current password the user has set is sufficient
* to meet the policy requirements (quality, minimum length) that have been
* requested.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call
* this method; if it has not, a security exception will be thrown.
- *
+ *
* @return Returns true if the password meets the current requirements,
* else false.
*/
@@ -346,11 +729,11 @@ public class DevicePolicyManager {
}
return false;
}
-
+
/**
* Retrieve the number of times the user has failed at entering a
* password since that last successful password entry.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to be able to call
* this method; if it has not, a security exception will be thrown.
@@ -373,14 +756,14 @@ public class DevicePolicyManager {
* watching for failed passwords and wiping the device, and requires
* that you request both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and
* {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}}.
- *
+ *
* <p>To implement any other policy (e.g. wiping data for a particular
* application only, erasing or revoking credentials, or reporting the
* failure to a server), you should implement
* {@link DeviceAdminReceiver#onPasswordFailed(Context, android.content.Intent)}
* instead. Do not use this API, because if the maximum count is reached,
* the device will be wiped immediately, and your callback will not be invoked.
- *
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param num The number of failed password attempts at which point the
* device will wipe its data.
@@ -394,7 +777,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* Retrieve the current maximum number of login attempts that are allowed
* before the device wipes itself, for all admins
@@ -412,13 +795,13 @@ public class DevicePolicyManager {
}
return 0;
}
-
+
/**
* Flag for {@link #resetPassword}: don't allow other admins to change
* the password again until the user has entered it.
*/
public static final int RESET_PASSWORD_REQUIRE_ENTRY = 0x0001;
-
+
/**
* Force a new device unlock password (the password needed to access the
* entire device, not for individual accounts) on the user. This takes
@@ -431,11 +814,11 @@ public class DevicePolicyManager {
* that the password may be a stronger quality (containing alphanumeric
* characters when the requested quality is only numeric), in which case
* the currently active quality will be increased to match.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD} to be able to call
* this method; if it has not, a security exception will be thrown.
- *
+ *
* @param password The new password for the user.
* @param flags May be 0 or {@link #RESET_PASSWORD_REQUIRE_ENTRY}.
* @return Returns true if the password was applied, or false if it is
@@ -451,16 +834,16 @@ public class DevicePolicyManager {
}
return false;
}
-
+
/**
* Called by an application that is administering the device to set the
* maximum time for user activity until the device will lock. This limits
* the length that the user can set. It takes effect immediately.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call
* this method; if it has not, a security exception will be thrown.
- *
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param timeMs The new desired maximum time to lock in milliseconds.
* A value of 0 means there is no restriction.
@@ -474,7 +857,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* Retrieve the current maximum time to unlock for all admins
* or a particular one.
@@ -491,11 +874,11 @@ public class DevicePolicyManager {
}
return 0;
}
-
+
/**
* Make the device lock immediately, as if the lock screen timeout has
* expired at the point of this call.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call
* this method; if it has not, a security exception will be thrown.
@@ -509,16 +892,16 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* Ask the user date be wiped. This will cause the device to reboot,
* erasing all user data while next booting up. External storage such
* as SD cards will not be erased.
- *
+ *
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call
* this method; if it has not, a security exception will be thrown.
- *
+ *
* @param flags Bit mask of additional options: currently must be 0.
*/
public void wipeData(int flags) {
@@ -530,7 +913,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* @hide
*/
@@ -543,7 +926,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* @hide
*/
@@ -556,10 +939,10 @@ public class DevicePolicyManager {
Log.w(TAG, "Unable to retrieve device policy " + cn, e);
return null;
}
-
+
ResolveInfo ri = new ResolveInfo();
ri.activityInfo = ai;
-
+
try {
return new DeviceAdminInfo(mContext, ri);
} catch (XmlPullParserException e) {
@@ -570,7 +953,7 @@ public class DevicePolicyManager {
return null;
}
}
-
+
/**
* @hide
*/
@@ -587,16 +970,18 @@ public class DevicePolicyManager {
/**
* @hide
*/
- public void setActivePasswordState(int quality, int length) {
+ public void setActivePasswordState(int quality, int length, int letters, int uppercase,
+ int lowercase, int numbers, int symbols, int nonletter) {
if (mService != null) {
try {
- mService.setActivePasswordState(quality, length);
+ mService.setActivePasswordState(quality, length, letters, uppercase, lowercase,
+ numbers, symbols, nonletter);
} catch (RemoteException e) {
Log.w(TAG, "Failed talking with device policy service", e);
}
}
}
-
+
/**
* @hide
*/
@@ -609,7 +994,7 @@ public class DevicePolicyManager {
}
}
}
-
+
/**
* @hide
*/
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 6fc4dc5..3ada95c 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -27,10 +27,31 @@ import android.os.RemoteCallback;
interface IDevicePolicyManager {
void setPasswordQuality(in ComponentName who, int quality);
int getPasswordQuality(in ComponentName who);
-
+
void setPasswordMinimumLength(in ComponentName who, int length);
int getPasswordMinimumLength(in ComponentName who);
+
+ void setPasswordMinimumUpperCase(in ComponentName who, int length);
+ int getPasswordMinimumUpperCase(in ComponentName who);
+
+ void setPasswordMinimumLowerCase(in ComponentName who, int length);
+ int getPasswordMinimumLowerCase(in ComponentName who);
+
+ void setPasswordMinimumLetters(in ComponentName who, int length);
+ int getPasswordMinimumLetters(in ComponentName who);
+
+ void setPasswordMinimumNumeric(in ComponentName who, int length);
+ int getPasswordMinimumNumeric(in ComponentName who);
+
+ void setPasswordMinimumSymbols(in ComponentName who, int length);
+ int getPasswordMinimumSymbols(in ComponentName who);
+
+ void setPasswordMinimumNonLetter(in ComponentName who, int length);
+ int getPasswordMinimumNonLetter(in ComponentName who);
+ void setPasswordHistoryLength(in ComponentName who, int length);
+ int getPasswordHistoryLength(in ComponentName who);
+
boolean isActivePasswordSufficient();
int getCurrentFailedPasswordAttempts();
@@ -53,7 +74,8 @@ interface IDevicePolicyManager {
void getRemoveWarning(in ComponentName policyReceiver, in RemoteCallback result);
void removeActiveAdmin(in ComponentName policyReceiver);
- void setActivePasswordState(int quality, int length);
+ void setActivePasswordState(int quality, int length, int letters, int uppercase, int lowercase,
+ int numbers, int symbols, int nonletter);
void reportFailedPasswordAttempt();
void reportSuccessfulPasswordAttempt();
}
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index b33b097..3c19ea3 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -23,9 +23,9 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
-import android.os.SystemClock;
-import android.os.Parcelable;
import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
@@ -275,6 +275,7 @@ public class AppWidgetHostView extends FrameLayout {
}
}
+ @Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (CROSSFADE) {
int alpha;
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index d2ab85e..3f12bf9 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -292,7 +292,15 @@ public class AppWidgetManager {
*/
public List<AppWidgetProviderInfo> getInstalledProviders() {
try {
- return sService.getInstalledProviders();
+ List<AppWidgetProviderInfo> providers = sService.getInstalledProviders();
+ for (AppWidgetProviderInfo info : providers) {
+ // Converting complex to dp.
+ info.minWidth =
+ TypedValue.complexToDimensionPixelSize(info.minWidth, mDisplayMetrics);
+ info.minHeight =
+ TypedValue.complexToDimensionPixelSize(info.minHeight, mDisplayMetrics);
+ }
+ return providers;
}
catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java
index cee2865..396e92d 100644
--- a/core/java/android/appwidget/AppWidgetProviderInfo.java
+++ b/core/java/android/appwidget/AppWidgetProviderInfo.java
@@ -110,6 +110,17 @@ public class AppWidgetProviderInfo implements Parcelable {
* @hide Pending API approval
*/
public String oldName;
+
+ /**
+ * A preview of what the AppWidget will look like after it's configured.
+ * If not supplied, the AppWidget's icon will be used.
+ *
+ * <p>This field corresponds to the <code>android:previewImage</code> attribute in
+ * the <code>&lt;receiver&gt;</code> element in the AndroidManifest.xml file.
+ *
+ * @hide Pending API approval
+ */
+ public int previewImage;
public AppWidgetProviderInfo() {
}
@@ -130,6 +141,7 @@ public class AppWidgetProviderInfo implements Parcelable {
}
this.label = in.readString();
this.icon = in.readInt();
+ this.previewImage = in.readInt();
}
@@ -152,6 +164,7 @@ public class AppWidgetProviderInfo implements Parcelable {
}
out.writeString(this.label);
out.writeInt(this.icon);
+ out.writeInt(this.previewImage);
}
public int describeContents() {
diff --git a/core/java/android/bluetooth/BluetoothClass.java b/core/java/android/bluetooth/BluetoothClass.java
index c7fea9e..0c9bab2 100644
--- a/core/java/android/bluetooth/BluetoothClass.java
+++ b/core/java/android/bluetooth/BluetoothClass.java
@@ -259,6 +259,8 @@ public final class BluetoothClass implements Parcelable {
public static final int PROFILE_A2DP = 1;
/** @hide */
public static final int PROFILE_OPP = 2;
+ /** @hide */
+ public static final int PROFILE_HID = 3;
/**
* Check class bits for possible bluetooth profile support.
@@ -324,6 +326,8 @@ public final class BluetoothClass implements Parcelable {
default:
return false;
}
+ } else if (profile == PROFILE_HID) {
+ return (getDeviceClass() & Device.Major.PERIPHERAL) == Device.Major.PERIPHERAL;
} else {
return false;
}
diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java
new file mode 100644
index 0000000..1793838
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothInputDevice.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Public API for controlling the Bluetooth HID (Input Device) Profile
+ *
+ * BluetoothInputDevice is a proxy object used to make calls to Bluetooth Service
+ * which handles the HID profile.
+ *
+ * Creating a BluetoothInputDevice object will initiate a binding with the
+ * Bluetooth service. Users of this object should call close() when they
+ * are finished, so that this proxy object can unbind from the service.
+ *
+ * Currently the Bluetooth service runs in the system server and this
+ * proxy object will be immediately bound to the service on construction.
+ *
+ * @hide
+ */
+public final class BluetoothInputDevice {
+ private static final String TAG = "BluetoothInputDevice";
+ private static final boolean DBG = false;
+
+ /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */
+ public static final String EXTRA_INPUT_DEVICE_STATE =
+ "android.bluetooth.inputdevice.extra.INPUT_DEVICE_STATE";
+ /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */
+ public static final String EXTRA_PREVIOUS_INPUT_DEVICE_STATE =
+ "android.bluetooth.inputdevice.extra.PREVIOUS_INPUT_DEVICE_STATE";
+
+ /** Indicates the state of an input device has changed.
+ * This intent will always contain EXTRA_INPUT_DEVICE_STATE,
+ * EXTRA_PREVIOUS_INPUT_DEVICE_STATE and BluetoothDevice.EXTRA_DEVICE
+ * extras.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_INPUT_DEVICE_STATE_CHANGED =
+ "android.bluetooth.inputdevice.action.INPUT_DEVICE_STATE_CHANGED";
+
+ public static final int STATE_DISCONNECTED = 0;
+ public static final int STATE_CONNECTING = 1;
+ public static final int STATE_CONNECTED = 2;
+ public static final int STATE_DISCONNECTING = 3;
+
+ /**
+ * Auto connection, incoming and outgoing connection are allowed at this
+ * priority level.
+ */
+ public static final int PRIORITY_AUTO_CONNECT = 1000;
+ /**
+ * Incoming and outgoing connection are allowed at this priority level
+ */
+ public static final int PRIORITY_ON = 100;
+ /**
+ * Connections to the device are not allowed at this priority level.
+ */
+ public static final int PRIORITY_OFF = 0;
+ /**
+ * Default priority level when the device is unpaired.
+ */
+ public static final int PRIORITY_UNDEFINED = -1;
+
+ private final IBluetooth mService;
+ private final Context mContext;
+
+ /**
+ * Create a BluetoothInputDevice proxy object for interacting with the local
+ * Bluetooth Service which handle the HID profile.
+ * @param c Context
+ */
+ public BluetoothInputDevice(Context c) {
+ mContext = c;
+
+ IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE);
+ if (b != null) {
+ mService = IBluetooth.Stub.asInterface(b);
+ } else {
+ Log.w(TAG, "Bluetooth Service not available!");
+
+ // Instead of throwing an exception which prevents people from going
+ // into Wireless settings in the emulator. Let it crash later when it is actually used.
+ mService = null;
+ }
+ }
+
+ /** Initiate a connection to an Input device.
+ *
+ * This function returns false on error and true if the connection
+ * attempt is being made.
+ *
+ * Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when the
+ * connection is completed.
+ * @param device Remote BT device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ public boolean connectInputDevice(BluetoothDevice device) {
+ if (DBG) log("connectInputDevice(" + device + ")");
+ try {
+ return mService.connectInputDevice(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /** Initiate disconnect from an Input Device.
+ * This function return false on error and true if the disconnection
+ * attempt is being made.
+ *
+ * Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when
+ * disconnect is completed.
+ *
+ * @param device Remote BT device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ public boolean disconnectInputDevice(BluetoothDevice device) {
+ if (DBG) log("disconnectInputDevice(" + device + ")");
+ try {
+ return mService.disconnectInputDevice(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /** Check if a specified InputDevice is connected.
+ *
+ * @param device Remote BT device.
+ * @return True if connected , false otherwise and on error.
+ * @hide
+ */
+ public boolean isInputDeviceConnected(BluetoothDevice device) {
+ if (DBG) log("isInputDeviceConnected(" + device + ")");
+ int state = getInputDeviceState(device);
+ if (state == STATE_CONNECTED) return true;
+ return false;
+ }
+
+ /** Check if any Input Device is connected.
+ *
+ * @return a unmodifiable set of connected Input Devices, or null on error.
+ * @hide
+ */
+ public Set<BluetoothDevice> getConnectedInputDevices() {
+ if (DBG) log("getConnectedInputDevices()");
+ try {
+ return Collections.unmodifiableSet(
+ new HashSet<BluetoothDevice>(
+ Arrays.asList(mService.getConnectedInputDevices())));
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return null;
+ }
+ }
+
+ /** Get the state of an Input Device.
+ *
+ * @param device Remote BT device.
+ * @return The current state of the Input Device
+ * @hide
+ */
+ public int getInputDeviceState(BluetoothDevice device) {
+ if (DBG) log("getInputDeviceState(" + device + ")");
+ try {
+ return mService.getInputDeviceState(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return STATE_DISCONNECTED;
+ }
+ }
+
+ /**
+ * Set priority of an input device.
+ *
+ * Priority is a non-negative integer. Priority can take the following
+ * values:
+ * {@link PRIORITY_ON}, {@link PRIORITY_OFF}, {@link PRIORITY_AUTO_CONNECT}
+ *
+ * @param device Paired device.
+ * @param priority Integer priority
+ * @return true if priority is set, false on error
+ */
+ public boolean setInputDevicePriority(BluetoothDevice device, int priority) {
+ if (DBG) log("setInputDevicePriority(" + device + ", " + priority + ")");
+ try {
+ return mService.setInputDevicePriority(device, priority);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get the priority associated with an Input Device.
+ *
+ * @param device Input Device
+ * @return non-negative priority, or negative error code on error.
+ */
+ public int getInputDevicePriority(BluetoothDevice device) {
+ if (DBG) log("getInputDevicePriority(" + device + ")");
+ try {
+ return mService.getInputDevicePriority(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return PRIORITY_OFF;
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java
index 4164a3d..f1ee907 100644
--- a/core/java/android/bluetooth/BluetoothUuid.java
+++ b/core/java/android/bluetooth/BluetoothUuid.java
@@ -49,6 +49,8 @@ public final class BluetoothUuid {
ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB");
public static final ParcelUuid ObexObjectPush =
ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb");
+ public static final ParcelUuid Hid =
+ ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb");
public static final ParcelUuid[] RESERVED_UUIDS = {
AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget,
@@ -82,6 +84,10 @@ public final class BluetoothUuid {
return uuid.equals(AvrcpTarget);
}
+ public static boolean isInputDevice(ParcelUuid uuid) {
+ return uuid.equals(Hid);
+ }
+
/**
* Returns true if ParcelUuid is present in uuidArray
*
diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl
index ea71034..75f093c 100644
--- a/core/java/android/bluetooth/IBluetooth.aidl
+++ b/core/java/android/bluetooth/IBluetooth.aidl
@@ -17,6 +17,7 @@
package android.bluetooth;
import android.bluetooth.IBluetoothCallback;
+import android.bluetooth.BluetoothDevice;
import android.os.ParcelUuid;
/**
@@ -72,4 +73,12 @@ interface IBluetooth
boolean connectHeadset(String address);
boolean disconnectHeadset(String address);
boolean notifyIncomingConnection(String address);
+
+ // HID profile APIs
+ boolean connectInputDevice(in BluetoothDevice device);
+ boolean disconnectInputDevice(in BluetoothDevice device);
+ BluetoothDevice[] getConnectedInputDevices(); // change to Set<> once AIDL supports
+ int getInputDeviceState(in BluetoothDevice device);
+ boolean setInputDevicePriority(in BluetoothDevice device, int priority);
+ int getInputDevicePriority(in BluetoothDevice device);
}
diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java
new file mode 100644
index 0000000..b19c072
--- /dev/null
+++ b/core/java/android/content/AsyncTaskLoader.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.AsyncTask;
+
+/**
+ * Abstract Loader that provides an {@link AsyncTask} to do the work.
+ *
+ * @param <D> the data type to be loaded.
+ */
+public abstract class AsyncTaskLoader<D> extends Loader<D> {
+ final class LoadTask extends AsyncTask<Void, Void, D> {
+
+ private D result;
+
+ /* Runs on a worker thread */
+ @Override
+ protected D doInBackground(Void... params) {
+ result = AsyncTaskLoader.this.loadInBackground();
+ return result;
+ }
+
+ /* Runs on the UI thread */
+ @Override
+ protected void onPostExecute(D data) {
+ AsyncTaskLoader.this.dispatchOnLoadComplete(data);
+ }
+
+ @Override
+ protected void onCancelled() {
+ AsyncTaskLoader.this.onCancelled(result);
+ }
+ }
+
+ LoadTask mTask;
+
+ public AsyncTaskLoader(Context context) {
+ super(context);
+ }
+
+ /**
+ * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+ * loaded data set and load a new one.
+ */
+ @Override
+ public void forceLoad() {
+ cancelLoad();
+ mTask = new LoadTask();
+ mTask.execute((Void[]) null);
+ }
+
+ /**
+ * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)}
+ * for more info.
+ *
+ * @return <tt>false</tt> if the task could not be canceled,
+ * typically because it has already completed normally, or
+ * because {@link #startLoading()} hasn't been called, and
+ * <tt>true</tt> otherwise
+ */
+ public boolean cancelLoad() {
+ if (mTask != null) {
+ boolean cancelled = mTask.cancel(false);
+ mTask = null;
+ return cancelled;
+ }
+ return false;
+ }
+
+ /**
+ * Called if the task was canceled before it was completed. Gives the class a chance
+ * to properly dispose of the result.
+ */
+ public void onCancelled(D data) {
+ }
+
+ void dispatchOnLoadComplete(D data) {
+ mTask = null;
+ deliverResult(data);
+ }
+
+ /**
+ * Called on a worker thread to perform the actual load. Implementations should not deliver the
+ * results directly, but should return them from this method, which will eventually end up
+ * calling deliverResult on the UI thread. If implementations need to process
+ * the results on the UI thread they may override deliverResult and do so
+ * there.
+ *
+ * @return the result of the load
+ */
+ public abstract D loadInBackground();
+}
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 69f7611..2ea0df96 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -44,9 +44,9 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.List;
import java.util.Random;
-import java.util.ArrayList;
/**
@@ -401,8 +401,7 @@ public abstract class ContentResolver {
/**
* Open a raw file descriptor to access data under a "content:" URI. This
* interacts with the underlying {@link ContentProvider#openAssetFile}
- * ContentProvider.openAssetFile()} method of the provider associated with the
- * given URI, to retrieve any file stored there.
+ * method of the provider associated with the given URI, to retrieve any file stored there.
*
* <h5>Accepts the following URI schemes:</h5>
* <ul>
@@ -1342,7 +1341,7 @@ public abstract class ContentResolver {
}
private final class CursorWrapperInner extends CursorWrapper {
- private IContentProvider mContentProvider;
+ private final IContentProvider mContentProvider;
public static final String TAG="CursorWrapperInner";
private boolean mCloseFlag = false;
@@ -1371,7 +1370,7 @@ public abstract class ContentResolver {
}
private final class ParcelFileDescriptorInner extends ParcelFileDescriptor {
- private IContentProvider mContentProvider;
+ private final IContentProvider mContentProvider;
public static final String TAG="ParcelFileDescriptorInner";
private boolean mReleaseProviderFlag = false;
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index a14bd8f..b49d801 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -21,6 +21,7 @@ import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.graphics.Bitmap;
@@ -588,6 +589,32 @@ public abstract class Context {
int mode, CursorFactory factory);
/**
+ * Open a new private SQLiteDatabase associated with this Context's
+ * application package. Creates the database file if it doesn't exist.
+ *
+ * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
+ * used to handle corruption when sqlite reports database corruption.</p>
+ *
+ * @param name The name (unique in the application package) of the database.
+ * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the
+ * default operation, {@link #MODE_WORLD_READABLE}
+ * and {@link #MODE_WORLD_WRITEABLE} to control permissions.
+ * @param factory An optional factory class that is called to instantiate a
+ * cursor when query is called.
+ * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database
+ * corruption. if null, {@link android.database.DefaultDatabaseErrorHandler} is assumed.
+ * @return The contents of a newly created database with the given name.
+ * @throws android.database.sqlite.SQLiteException if the database file could not be opened.
+ *
+ * @see #MODE_PRIVATE
+ * @see #MODE_WORLD_READABLE
+ * @see #MODE_WORLD_WRITEABLE
+ * @see #deleteDatabase
+ */
+ public abstract SQLiteDatabase openOrCreateDatabase(String name,
+ int mode, CursorFactory factory, DatabaseErrorHandler errorHandler);
+
+ /**
* Delete an existing private SQLiteDatabase associated with this Context's
* application package.
*
@@ -1372,7 +1399,6 @@ public abstract class Context {
public static final String SENSOR_SERVICE = "sensor";
/**
- * @hide
* Use with {@link #getSystemService} to retrieve a {@link
* android.os.storage.StorageManager} for accesssing system storage
* functions.
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index a447108..3f5d215 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -20,6 +20,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
+import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.graphics.Bitmap;
@@ -204,6 +205,12 @@ public class ContextWrapper extends Context {
}
@Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory,
+ DatabaseErrorHandler errorHandler) {
+ return mBase.openOrCreateDatabase(name, mode, factory, errorHandler);
+ }
+
+ @Override
public boolean deleteDatabase(String name) {
return mBase.deleteDatabase(name);
}
diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java
new file mode 100644
index 0000000..e230394
--- /dev/null
+++ b/core/java/android/content/CursorLoader.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * A loader that queries the {@link ContentResolver} and returns a {@link Cursor}.
+ */
+public class CursorLoader extends AsyncTaskLoader<Cursor> {
+ Cursor mCursor;
+ ForceLoadContentObserver mObserver;
+ boolean mStopped;
+ Uri mUri;
+ String[] mProjection;
+ String mSelection;
+ String[] mSelectionArgs;
+ String mSortOrder;
+
+ /* Runs on a worker thread */
+ @Override
+ public Cursor loadInBackground() {
+ Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
+ mSelectionArgs, mSortOrder);
+ // Ensure the cursor window is filled
+ if (cursor != null) {
+ cursor.getCount();
+ cursor.registerContentObserver(mObserver);
+ }
+ return cursor;
+ }
+
+ /* Runs on the UI thread */
+ @Override
+ public void deliverResult(Cursor cursor) {
+ if (mStopped) {
+ // An async query came in while the loader is stopped
+ cursor.close();
+ return;
+ }
+ mCursor = cursor;
+ super.deliverResult(cursor);
+ }
+
+ public CursorLoader(Context context, Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ super(context);
+ mObserver = new ForceLoadContentObserver();
+ mUri = uri;
+ mProjection = projection;
+ mSelection = selection;
+ mSelectionArgs = selectionArgs;
+ mSortOrder = sortOrder;
+ }
+
+ /**
+ * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+ * will be called on the UI thread. If a previous load has been completed and is still valid
+ * the result may be passed to the callbacks immediately.
+ *
+ * Must be called from the UI thread
+ */
+ @Override
+ public void startLoading() {
+ mStopped = false;
+
+ if (mCursor != null) {
+ deliverResult(mCursor);
+ } else {
+ forceLoad();
+ }
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ @Override
+ public void stopLoading() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mCursor.close();
+ mCursor = null;
+ }
+
+ // Attempt to cancel the current load task if possible.
+ cancelLoad();
+
+ // Make sure that any outstanding loads clean themselves up properly
+ mStopped = true;
+ }
+
+ @Override
+ public void onCancelled(Cursor cursor) {
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // Ensure the loader is stopped
+ stopLoading();
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public void setUri(Uri uri) {
+ mUri = uri;
+ }
+
+ public String[] getProjection() {
+ return mProjection;
+ }
+
+ public void setProjection(String[] projection) {
+ mProjection = projection;
+ }
+
+ public String getSelection() {
+ return mSelection;
+ }
+
+ public void setSelection(String selection) {
+ mSelection = selection;
+ }
+
+ public String[] getSelectionArgs() {
+ return mSelectionArgs;
+ }
+
+ public void setSelectionArgs(String[] selectionArgs) {
+ mSelectionArgs = selectionArgs;
+ }
+
+ public String getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(String sortOrder) {
+ mSortOrder = sortOrder;
+ }
+}
diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java
new file mode 100644
index 0000000..db40e48
--- /dev/null
+++ b/core/java/android/content/Loader.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.ContentObserver;
+import android.os.Handler;
+
+/**
+ * An abstract class that performs asynchronous loading of data. While Loaders are active
+ * they should monitor the source of their data and deliver new results when the contents
+ * change.
+ *
+ * @param <D> The result returned when the load is complete
+ */
+public abstract class Loader<D> {
+ int mId;
+ OnLoadCompleteListener<D> mListener;
+ Context mContext;
+
+ public final class ForceLoadContentObserver extends ContentObserver {
+ public ForceLoadContentObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ forceLoad();
+ }
+ }
+
+ public interface OnLoadCompleteListener<D> {
+ /**
+ * Called on the thread that created the Loader when the load is complete.
+ *
+ * @param loader the loader that completed the load
+ * @param data the result of the load
+ */
+ public void onLoadComplete(Loader<D> loader, D data);
+ }
+
+ /**
+ * Stores away the application context associated with context. Since Loaders can be used
+ * across multiple activities it's dangerous to store the context directly.
+ *
+ * @param context used to retrieve the application context.
+ */
+ public Loader(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ /**
+ * Sends the result of the load to the registered listener. Should only be called by subclasses.
+ *
+ * Must be called from the UI thread.
+ *
+ * @param data the result of the load
+ */
+ public void deliverResult(D data) {
+ if (mListener != null) {
+ mListener.onLoadComplete(this, data);
+ }
+ }
+
+ /**
+ * @return an application context retrieved from the Context passed to the constructor.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * @return the ID of this loader
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Registers a class that will receive callbacks when a load is complete. The callbacks will
+ * be called on the UI thread so it's safe to pass the results to widgets.
+ *
+ * Must be called from the UI thread
+ */
+ public void registerListener(int id, OnLoadCompleteListener<D> listener) {
+ if (mListener != null) {
+ throw new IllegalStateException("There is already a listener registered");
+ }
+ mListener = listener;
+ mId = id;
+ }
+
+ /**
+ * Must be called from the UI thread
+ */
+ public void unregisterListener(OnLoadCompleteListener<D> listener) {
+ if (mListener == null) {
+ throw new IllegalStateException("No listener register");
+ }
+ if (mListener != listener) {
+ throw new IllegalArgumentException("Attempting to unregister the wrong listener");
+ }
+ mListener = null;
+ }
+
+ /**
+ * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+ * will be called on the UI thread. If a previous load has been completed and is still valid
+ * the result may be passed to the callbacks immediately. The loader will monitor the source of
+ * the data set and may deliver future callbacks if the source changes. Calling
+ * {@link #stopLoading} will stop the delivery of callbacks.
+ *
+ * Must be called from the UI thread
+ */
+ public abstract void startLoading();
+
+ /**
+ * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+ * loaded data set and load a new one.
+ */
+ public abstract void forceLoad();
+
+ /**
+ * Stops delivery of updates until the next time {@link #startLoading()} is called
+ *
+ * Must be called from the UI thread
+ */
+ public abstract void stopLoading();
+
+ /**
+ * Destroys the loader and frees its resources, making it unusable.
+ *
+ * Must be called from the UI thread
+ */
+ public abstract void destroy();
+} \ No newline at end of file
diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java
index a15e29e..5847216 100644
--- a/core/java/android/content/SharedPreferences.java
+++ b/core/java/android/content/SharedPreferences.java
@@ -17,6 +17,7 @@
package android.content;
import java.util.Map;
+import java.util.Set;
/**
* Interface for accessing and modifying preference data returned by {@link
@@ -69,6 +70,17 @@ public interface SharedPreferences {
Editor putString(String key, String value);
/**
+ * Set a set of String values in the preferences editor, to be written
+ * back once {@link #commit} is called.
+ *
+ * @param key The name of the preference to modify.
+ * @param values The new values for the preference.
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor putStringSet(String key, Set<String> values);
+
+ /**
* Set an int value in the preferences editor, to be written back once
* {@link #commit} is called.
*
@@ -186,6 +198,20 @@ public interface SharedPreferences {
String getString(String key, String defValue);
/**
+ * Retrieve a set of String values from the preferences.
+ *
+ * @param key The name of the preference to retrieve.
+ * @param defValues Values to return if this preference does not exist.
+ *
+ * @return Returns the preference values if they exist, or defValues.
+ * Throws ClassCastException if there is a preference with this name
+ * that is not a Set.
+ *
+ * @throws ClassCastException
+ */
+ Set<String> getStringSet(String key, Set<String> defValues);
+
+ /**
* Retrieve an int value from the preferences.
*
* @param key The name of the preference to retrieve.
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
index d0b67cc..7f749bb 100644
--- a/core/java/android/content/SyncManager.java
+++ b/core/java/android/content/SyncManager.java
@@ -16,6 +16,8 @@
package android.content;
+import com.google.android.collect.Maps;
+
import com.android.internal.R;
import com.android.internal.util.ArrayUtils;
@@ -55,6 +57,7 @@ import android.util.Pair;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
@@ -126,14 +129,13 @@ public class SyncManager implements OnAccountsUpdateListener {
private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000;
- private static final String SYNC_WAKE_LOCK = "SyncManagerSyncWakeLock";
+ private static final String SYNC_WAKE_LOCK_PREFIX = "SyncWakeLock";
private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock";
private Context mContext;
private volatile Account[] mAccounts = INITIAL_ACCOUNTS_ARRAY;
- volatile private PowerManager.WakeLock mSyncWakeLock;
volatile private PowerManager.WakeLock mHandleAlarmWakeLock;
volatile private boolean mDataConnectionIsConnected = false;
volatile private boolean mStorageIsLow = false;
@@ -195,6 +197,8 @@ public class SyncManager implements OnAccountsUpdateListener {
private static final Account[] INITIAL_ACCOUNTS_ARRAY = new Account[0];
+ private final PowerManager mPowerManager;
+
public void onAccountsUpdated(Account[] accounts) {
// remember if this was the first time this was called after an update
final boolean justBootedUp = mAccounts == INITIAL_ACCOUNTS_ARRAY;
@@ -356,15 +360,13 @@ public class SyncManager implements OnAccountsUpdateListener {
} else {
mNotificationMgr = null;
}
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK);
- mSyncWakeLock.setReferenceCounted(false);
+ mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
// This WakeLock is used to ensure that we stay awake between the time that we receive
// a sync alarm notification and when we finish processing it. We need to do this
// because we don't do the work in the alarm handler, rather we do it in a message
// handler.
- mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
HANDLE_SYNC_ALARM_WAKE_LOCK);
mHandleAlarmWakeLock.setReferenceCounted(false);
@@ -1302,6 +1304,9 @@ public class SyncManager implements OnAccountsUpdateListener {
public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo();
private Long mAlarmScheduleTime = null;
public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker();
+ private PowerManager.WakeLock mSyncWakeLock;
+ private final HashMap<Pair<String, String>, PowerManager.WakeLock> mWakeLocks =
+ Maps.newHashMap();
// used to track if we have installed the error notification so that we don't reinstall
// it if sync is still failing
@@ -1315,6 +1320,18 @@ public class SyncManager implements OnAccountsUpdateListener {
}
}
+ private PowerManager.WakeLock getSyncWakeLock(String accountType, String authority) {
+ final Pair<String, String> wakeLockKey = Pair.create(accountType, authority);
+ PowerManager.WakeLock wakeLock = mWakeLocks.get(wakeLockKey);
+ if (wakeLock == null) {
+ final String name = SYNC_WAKE_LOCK_PREFIX + "_" + authority + "_" + accountType;
+ wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
+ wakeLock.setReferenceCounted(false);
+ mWakeLocks.put(wakeLockKey, wakeLock);
+ }
+ return wakeLock;
+ }
+
private void waitUntilReadyToRun() {
CountDownLatch latch = mReadyToRunLatch;
if (latch != null) {
@@ -1477,8 +1494,9 @@ public class SyncManager implements OnAccountsUpdateListener {
}
} finally {
final boolean isSyncInProgress = mActiveSyncContext != null;
- if (!isSyncInProgress) {
+ if (!isSyncInProgress && mSyncWakeLock != null) {
mSyncWakeLock.release();
+ mSyncWakeLock = null;
}
manageSyncNotification();
manageErrorNotification();
@@ -1704,7 +1722,26 @@ public class SyncManager implements OnAccountsUpdateListener {
return;
}
- mSyncWakeLock.acquire();
+ // Find the wakelock for this account and authority and store it in mSyncWakeLock.
+ // Be sure to release the previous wakelock so that we don't end up with it being
+ // held until it is used again.
+ // There are a couple tricky things about this code:
+ // - make sure that we acquire the new wakelock before releasing the old one,
+ // otherwise the device might go to sleep as soon as we release it.
+ // - since we use non-reference counted wakelocks we have to be sure not to do
+ // the release if the wakelock didn't change. Othewise we would do an
+ // acquire followed by a release on the same lock, resulting in no lock
+ // being held.
+ PowerManager.WakeLock oldWakeLock = mSyncWakeLock;
+ try {
+ mSyncWakeLock = getSyncWakeLock(op.account.type, op.authority);
+ mSyncWakeLock.acquire();
+ } finally {
+ if (oldWakeLock != null && oldWakeLock != mSyncWakeLock) {
+ oldWakeLock.release();
+ }
+ }
+
// no need to schedule an alarm, as that will be done by our caller.
// the next step will occur when we get either a timeout or a
diff --git a/core/java/android/content/XmlDocumentProvider.java b/core/java/android/content/XmlDocumentProvider.java
new file mode 100644
index 0000000..153ad38
--- /dev/null
+++ b/core/java/android/content/XmlDocumentProvider.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.content.ContentResolver.OpenResourceIdResult;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.util.Log;
+import android.widget.CursorAdapter;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.BitSet;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ * A read-only content provider which extracts data out of an XML document.
+ *
+ * <p>A XPath-like selection pattern is used to select some nodes in the XML document. Each such
+ * node will create a row in the {@link Cursor} result.</p>
+ *
+ * Each row is then populated with columns that are also defined as XPath-like projections. These
+ * projections fetch attributes values or text in the matching row node or its children.
+ *
+ * <p>To add this provider in your application, you should add its declaration to your application
+ * manifest:
+ * <pre class="prettyprint">
+ * &lt;provider android:name="android.content.XmlDocumentProvider" android:authorities="xmldocument" /&gt;
+ * </pre>
+ * </p>
+ *
+ * <h2>Node selection syntax</h2>
+ * The node selection syntax is made of the concatenation of an arbitrary number (at least one) of
+ * <code>/node_name</code> node selection patterns.
+ *
+ * <p>The <code>/root/child1/child2</code> pattern will for instance match all nodes named
+ * <code>child2</code> which are children of a node named <code>child1</code> which are themselves
+ * children of a root node named <code>root</code>.</p>
+ *
+ * Any <code>/</code> separator in the previous expression can be replaced by a <code>//</code>
+ * separator instead, which indicated a <i>descendant</i> instead of a child.
+ *
+ * <p>The <code>//node1//node2</code> pattern will for instance match all nodes named
+ * <code>node2</code> which are descendant of a node named <code>node1</code> located anywhere in
+ * the document hierarchy.</p>
+ *
+ * Node names can contain namespaces in the form <code>namespace:node</code>.
+ *
+ * <h2>Projection syntax</h2>
+ * For every selected node, the projection will then extract actual data from this node and its
+ * descendant.
+ *
+ * <p>Use a syntax similar to the selection syntax described above to select the text associated
+ * with a child of the selected node. The implicit root of this projection pattern is the selected
+ * node. <code>/</code> will hence refer to the text of the selected node, while
+ * <code>/child1</code> will fetch the text of its child named <code>child1</code> and
+ * <code>//child1</code> will match any <i>descendant</i> named <code>child1</code>. If several
+ * nodes match the projection pattern, their texts are appended as a result.</p>
+ *
+ * A projection can also fetch any node attribute by appending a <code>@attribute_name</code>
+ * pattern to the previously described syntax. <code>//child1@price</code> will for instance match
+ * the attribute <code>price</code> of any <code>child1</code> descendant.
+ *
+ * <p>If a projection does not match any node/attribute, its associated value will be an empty
+ * string.</p>
+ *
+ * <h2>Example</h2>
+ * Using the following XML document:
+ * <pre class="prettyprint">
+ * &lt;library&gt;
+ * &lt;book id="EH94"&gt;
+ * &lt;title&gt;The Old Man and the Sea&lt;/title&gt;
+ * &lt;author&gt;Ernest Hemingway&lt;/author&gt;
+ * &lt;/book&gt;
+ * &lt;book id="XX10"&gt;
+ * &lt;title&gt;The Arabian Nights: Tales of 1,001 Nights&lt;/title&gt;
+ * &lt;/book&gt;
+ * &lt;no-id&gt;
+ * &lt;book&gt;
+ * &lt;title&gt;Animal Farm&lt;/title&gt;
+ * &lt;author&gt;George Orwell&lt;/author&gt;
+ * &lt;/book&gt;
+ * &lt;/no-id&gt;
+ * &lt;/library&gt;
+ * </pre>
+ * A selection pattern of <code>/library//book</code> will match the three book entries (while
+ * <code>/library/book</code> will only match the first two ones).
+ *
+ * <p>Defining the projections as <code>/title</code>, <code>/author</code> and <code>@id</code>
+ * will retrieve the associated data. Note that the author of the second book as well as the id of
+ * the third are empty strings.
+ */
+public class XmlDocumentProvider extends ContentProvider {
+ /*
+ * Ideas for improvement:
+ * - Expand XPath-like syntax to allow for [nb] child number selector
+ * - Address the starting . bug in AbstractCursor which prevents a true XPath syntax.
+ * - Provide an alternative to concatenation when several node match (list-like).
+ * - Support namespaces in attribute names.
+ * - Incremental Cursor creation, pagination
+ */
+ private static final String LOG_TAG = "XmlDocumentProvider";
+ private AndroidHttpClient mHttpClient;
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ /**
+ * Query data from the XML document referenced in the URI.
+ *
+ * <p>The XML document can be a local resource or a file that will be downloaded from the
+ * Internet. In the latter case, your application needs to request the INTERNET permission in
+ * its manifest.</p>
+ *
+ * The URI will be of the form <code>content://xmldocument/?resource=R.xml.myFile</code> for a
+ * local resource. <code>xmldocument</code> should match the authority declared for this
+ * provider in your manifest. Internet documents are referenced using
+ * <code>content://xmldocument/?url=</code> followed by an encoded version of the URL of your
+ * document (see {@link Uri#encode(String)}).
+ *
+ * <p>The number of columns of the resulting Cursor is equal to the size of the projection
+ * array plus one, named <code>_id</code> which will contain a unique row id (allowing the
+ * Cursor to be used with a {@link CursorAdapter}). The other columns' names are the projection
+ * patterns.</p>
+ *
+ * @param uri The URI of your local resource or Internet document.
+ * @param projection A set of patterns that will be used to extract data from each selected
+ * node. See class documentation for pattern syntax.
+ * @param selection A selection pattern which will select the nodes that will create the
+ * Cursor's rows. See class documentation for pattern syntax.
+ * @param selectionArgs This parameter is ignored.
+ * @param sortOrder The row order in the resulting cursor is determined from the node order in
+ * the XML document. This parameter is ignored.
+ * @return A Cursor or null in case of error.
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+
+ XmlPullParser parser = null;
+ mHttpClient = null;
+
+ final String url = uri.getQueryParameter("url");
+ if (url != null) {
+ parser = getUriXmlPullParser(url);
+ } else {
+ final String resource = uri.getQueryParameter("resource");
+ if (resource != null) {
+ Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+ getContext().getPackageName() + "/" + resource);
+ parser = getResourceXmlPullParser(resourceUri);
+ }
+ }
+
+ if (parser != null) {
+ XMLCursor xmlCursor = new XMLCursor(selection, projection);
+ try {
+ xmlCursor.parseWith(parser);
+ return xmlCursor;
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "I/O error while parsing XML " + uri, e);
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Error while parsing XML " + uri, e);
+ } finally {
+ if (mHttpClient != null) {
+ mHttpClient.close();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an XmlPullParser for the provided URL. Can be overloaded to provide your own parser.
+ * @param url The URL of the XML document that is to be parsed.
+ * @return An XmlPullParser on this document.
+ */
+ protected XmlPullParser getUriXmlPullParser(String url) {
+ XmlPullParser parser = null;
+ try {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ parser = factory.newPullParser();
+ } catch (XmlPullParserException e) {
+ Log.e(LOG_TAG, "Unable to create XmlPullParser", e);
+ return null;
+ }
+
+ InputStream inputStream = null;
+ try {
+ final HttpGet get = new HttpGet(url);
+ mHttpClient = AndroidHttpClient.newInstance("Android");
+ HttpResponse response = mHttpClient.execute(get);
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ inputStream = entity.getContent();
+ }
+ }
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Error while retrieving XML file " + url, e);
+ return null;
+ }
+
+ try {
+ parser.setInput(inputStream, null);
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Error while reading XML file from " + url, e);
+ return null;
+ }
+
+ return parser;
+ }
+
+ /**
+ * Creates an XmlPullParser for the provided local resource. Can be overloaded to provide your
+ * own parser.
+ * @param resourceUri A fully qualified resource name referencing a local XML resource.
+ * @return An XmlPullParser on this resource.
+ */
+ protected XmlPullParser getResourceXmlPullParser(Uri resourceUri) {
+ OpenResourceIdResult resourceId;
+ try {
+ resourceId = getContext().getContentResolver().getResourceId(resourceUri);
+ return resourceId.r.getXml(resourceId.id);
+ } catch (FileNotFoundException e) {
+ Log.w(LOG_TAG, "XML resource not found: " + resourceUri.toString(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns "vnd.android.cursor.dir/xmldoc".
+ */
+ @Override
+ public String getType(Uri uri) {
+ return "vnd.android.cursor.dir/xmldoc";
+ }
+
+ /**
+ * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
+ **/
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
+ **/
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
+ **/
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static class XMLCursor extends MatrixCursor {
+ private final Pattern mSelectionPattern;
+ private Pattern[] mProjectionPatterns;
+ private String[] mAttributeNames;
+ private String[] mCurrentValues;
+ private BitSet[] mActiveTextDepthMask;
+ private final int mNumberOfProjections;
+
+ public XMLCursor(String selection, String[] projections) {
+ super(projections);
+ // The first column in projections is used for the _ID
+ mNumberOfProjections = projections.length - 1;
+ mSelectionPattern = createPattern(selection);
+ createProjectionPattern(projections);
+ }
+
+ private Pattern createPattern(String input) {
+ String pattern = input.replaceAll("//", "/(.*/|)").replaceAll("^/", "^/") + "$";
+ return Pattern.compile(pattern);
+ }
+
+ private void createProjectionPattern(String[] projections) {
+ mProjectionPatterns = new Pattern[mNumberOfProjections];
+ mAttributeNames = new String[mNumberOfProjections];
+ mActiveTextDepthMask = new BitSet[mNumberOfProjections];
+ // Add a column to store _ID
+ mCurrentValues = new String[mNumberOfProjections + 1];
+
+ for (int i=0; i<mNumberOfProjections; i++) {
+ mActiveTextDepthMask[i] = new BitSet();
+ String projection = projections[i + 1]; // +1 to skip the _ID column
+ int atIndex = projection.lastIndexOf('@', projection.length());
+ if (atIndex >= 0) {
+ mAttributeNames[i] = projection.substring(atIndex+1);
+ projection = projection.substring(0, atIndex);
+ } else {
+ mAttributeNames[i] = null;
+ }
+
+ // Conforms to XPath standard: reference to local context starts with a .
+ if (projection.charAt(0) == '.') {
+ projection = projection.substring(1);
+ }
+ mProjectionPatterns[i] = createPattern(projection);
+ }
+ }
+
+ public void parseWith(XmlPullParser parser) throws IOException, XmlPullParserException {
+ StringBuilder path = new StringBuilder();
+ Stack<Integer> pathLengthStack = new Stack<Integer>();
+
+ // There are two parsing mode: in root mode, rootPath is updated and nodes matching
+ // selectionPattern are searched for and currentNodeDepth is negative.
+ // When a node matching selectionPattern is found, currentNodeDepth is set to 0 and
+ // updated as children are parsed and projectionPatterns are searched in nodePath.
+ int currentNodeDepth = -1;
+
+ // Index where local selected node path starts from in path
+ int currentNodePathStartIndex = 0;
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ // Update path
+ pathLengthStack.push(path.length());
+ path.append('/');
+ String prefix = null;
+ try {
+ // getPrefix is not supported by local Xml resource parser
+ prefix = parser.getPrefix();
+ } catch (RuntimeException e) {
+ prefix = null;
+ }
+ if (prefix != null) {
+ path.append(prefix);
+ path.append(':');
+ }
+ path.append(parser.getName());
+
+ if (currentNodeDepth >= 0) {
+ currentNodeDepth++;
+ } else {
+ // A node matching selection is found: initialize child parsing mode
+ if (mSelectionPattern.matcher(path.toString()).matches()) {
+ currentNodeDepth = 0;
+ currentNodePathStartIndex = path.length();
+ mCurrentValues[0] = Integer.toString(getCount()); // _ID
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ // Reset values to default (empty string)
+ mCurrentValues[i + 1] = "";
+ mActiveTextDepthMask[i].clear();
+ }
+ }
+ }
+
+ // This test has to be separated from the previous one as currentNodeDepth can
+ // be modified above (when a node matching selection is found).
+ if (currentNodeDepth >= 0) {
+ final String localNodePath = path.substring(currentNodePathStartIndex);
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ if (mProjectionPatterns[i].matcher(localNodePath).matches()) {
+ String attribute = mAttributeNames[i];
+ if (attribute != null) {
+ mCurrentValues[i + 1] =
+ parser.getAttributeValue(null, attribute);
+ } else {
+ mActiveTextDepthMask[i].set(currentNodeDepth, true);
+ }
+ }
+ }
+ }
+
+ } else if (eventType == XmlPullParser.END_TAG) {
+ // Pop last node from path
+ final int length = pathLengthStack.pop();
+ path.setLength(length);
+
+ if (currentNodeDepth >= 0) {
+ if (currentNodeDepth == 0) {
+ // Leaving a selection matching node: add a new row with results
+ addRow(mCurrentValues);
+ } else {
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ mActiveTextDepthMask[i].set(currentNodeDepth, false);
+ }
+ }
+ currentNodeDepth--;
+ }
+
+ } else if ((eventType == XmlPullParser.TEXT) && (!parser.isWhitespace())) {
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ if ((currentNodeDepth >= 0) &&
+ (mActiveTextDepthMask[i].get(currentNodeDepth))) {
+ mCurrentValues[i + 1] += parser.getText();
+ }
+ }
+ }
+
+ eventType = parser.next();
+ }
+ }
+ }
+}
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 7901b155..35f22dc 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -284,6 +284,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
public static final int FLAG_HEAVY_WEIGHT = 1<<20;
/**
+ * Value for {@link #flags}: true when the application's rendering should
+ * be hardware accelerated.
+ */
+ public static final int FLAG_HARDWARE_ACCELERATED = 1<<21;
+
+ /**
* Value for {@link #flags}: this is true if the application has set
* its android:neverEncrypt to true, false otherwise. It is used to specify
* that this package specifically "opts-out" of a secured file system solution,
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 5dc41d2..6098617 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -1536,6 +1536,12 @@ public class PackageParser {
}
if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_hardwareAccelerated,
+ false)) {
+ ai.flags |= ApplicationInfo.FLAG_HARDWARE_ACCELERATED;
+ }
+
+ if (sa.getBoolean(
com.android.internal.R.styleable.AndroidManifestApplication_hasCode,
true)) {
ai.flags |= ApplicationInfo.FLAG_HAS_CODE;
diff --git a/core/java/android/content/res/PluralRules.java b/core/java/android/content/res/PluralRules.java
deleted file mode 100644
index 2dce3c1..0000000
--- a/core/java/android/content/res/PluralRules.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.content.res;
-
-import java.util.Locale;
-
-/*
- * Yuck-o. This is not the right way to implement this. When the ICU PluralRules
- * object has been integrated to android, we should switch to that. For now, yuck-o.
- */
-
-abstract class PluralRules {
-
- static final int QUANTITY_OTHER = 0x0000;
- static final int QUANTITY_ZERO = 0x0001;
- static final int QUANTITY_ONE = 0x0002;
- static final int QUANTITY_TWO = 0x0004;
- static final int QUANTITY_FEW = 0x0008;
- static final int QUANTITY_MANY = 0x0010;
-
- static final int ID_OTHER = 0x01000004;
-
- abstract int quantityForNumber(int n);
-
- final int attrForNumber(int n) {
- return PluralRules.attrForQuantity(quantityForNumber(n));
- }
-
- static final int attrForQuantity(int quantity) {
- // see include/utils/ResourceTypes.h
- switch (quantity) {
- case QUANTITY_ZERO: return 0x01000005;
- case QUANTITY_ONE: return 0x01000006;
- case QUANTITY_TWO: return 0x01000007;
- case QUANTITY_FEW: return 0x01000008;
- case QUANTITY_MANY: return 0x01000009;
- default: return ID_OTHER;
- }
- }
-
- static final String stringForQuantity(int quantity) {
- switch (quantity) {
- case QUANTITY_ZERO:
- return "zero";
- case QUANTITY_ONE:
- return "one";
- case QUANTITY_TWO:
- return "two";
- case QUANTITY_FEW:
- return "few";
- case QUANTITY_MANY:
- return "many";
- default:
- return "other";
- }
- }
-
- static final PluralRules ruleForLocale(Locale locale) {
- String lang = locale.getLanguage();
- if ("cs".equals(lang)) {
- if (cs == null) cs = new cs();
- return cs;
- }
- else {
- if (en == null) en = new en();
- return en;
- }
- }
-
- private static PluralRules cs;
- private static class cs extends PluralRules {
- int quantityForNumber(int n) {
- if (n == 1) {
- return QUANTITY_ONE;
- }
- else if (n >= 2 && n <= 4) {
- return QUANTITY_FEW;
- }
- else {
- return QUANTITY_OTHER;
- }
- }
- }
-
- private static PluralRules en;
- private static class en extends PluralRules {
- int quantityForNumber(int n) {
- if (n == 1) {
- return QUANTITY_ONE;
- }
- else {
- return QUANTITY_OTHER;
- }
- }
- }
-}
-
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 0608cc0..5ac55c4 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -16,7 +16,6 @@
package android.content.res;
-
import com.android.internal.util.XmlUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -41,6 +40,8 @@ import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Locale;
+import libcore.icu.NativePluralRules;
+
/**
* Class for accessing an application's resources. This sits on top of the
* asset manager of the application (accessible through getAssets()) and
@@ -52,6 +53,8 @@ public class Resources {
private static final boolean DEBUG_CONFIG = false;
private static final boolean TRACE_FOR_PRELOAD = false;
+ private static final int ID_OTHER = 0x01000004;
+
// Use the current SDK version code. If we are a development build,
// also allow the previous SDK version + 1.
private static final int sSdkVersion = Build.VERSION.SDK_INT
@@ -86,7 +89,7 @@ public class Resources {
/*package*/ final AssetManager mAssets;
private final Configuration mConfiguration = new Configuration();
/*package*/ final DisplayMetrics mMetrics = new DisplayMetrics();
- PluralRules mPluralRule;
+ private NativePluralRules mPluralRule;
private CompatibilityInfo mCompatibilityInfo;
private Display mDefaultDisplay;
@@ -203,9 +206,17 @@ public class Resources {
}
/**
+ * Return the character sequence associated with a particular resource ID for a particular
+ * numerical quantity.
+ *
+ * <p>See <a href="{@docRoot}guide/topics/resources/string-resource.html#Plurals">String
+ * Resources</a> for more on quantity strings.
+ *
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
+ * @param quantity The number used to get the correct string for the current language's
+ * plural rules.
*
* @throws NotFoundException Throws NotFoundException if the given ID does not exist.
*
@@ -213,29 +224,52 @@ public class Resources {
* possibly styled text information.
*/
public CharSequence getQuantityText(int id, int quantity) throws NotFoundException {
- PluralRules rule = getPluralRule();
- CharSequence res = mAssets.getResourceBagText(id, rule.attrForNumber(quantity));
+ NativePluralRules rule = getPluralRule();
+ CharSequence res = mAssets.getResourceBagText(id,
+ attrForQuantityCode(rule.quantityForInt(quantity)));
if (res != null) {
return res;
}
- res = mAssets.getResourceBagText(id, PluralRules.ID_OTHER);
+ res = mAssets.getResourceBagText(id, ID_OTHER);
if (res != null) {
return res;
}
throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id)
+ " quantity=" + quantity
- + " item=" + PluralRules.stringForQuantity(rule.quantityForNumber(quantity)));
+ + " item=" + stringForQuantityCode(rule.quantityForInt(quantity)));
}
- private PluralRules getPluralRule() {
+ private NativePluralRules getPluralRule() {
synchronized (mSync) {
if (mPluralRule == null) {
- mPluralRule = PluralRules.ruleForLocale(mConfiguration.locale);
+ mPluralRule = NativePluralRules.forLocale(mConfiguration.locale);
}
return mPluralRule;
}
}
+ private static int attrForQuantityCode(int quantityCode) {
+ switch (quantityCode) {
+ case NativePluralRules.ZERO: return 0x01000005;
+ case NativePluralRules.ONE: return 0x01000006;
+ case NativePluralRules.TWO: return 0x01000007;
+ case NativePluralRules.FEW: return 0x01000008;
+ case NativePluralRules.MANY: return 0x01000009;
+ default: return ID_OTHER;
+ }
+ }
+
+ private static String stringForQuantityCode(int quantityCode) {
+ switch (quantityCode) {
+ case NativePluralRules.ZERO: return "zero";
+ case NativePluralRules.ONE: return "one";
+ case NativePluralRules.TWO: return "two";
+ case NativePluralRules.FEW: return "few";
+ case NativePluralRules.MANY: return "many";
+ default: return "other";
+ }
+ }
+
/**
* Return the string value associated with a particular resource ID. It
* will be stripped of any styled text information.
@@ -290,6 +324,9 @@ public class Resources {
* stripped of any styled text information.
* {@more}
*
+ * <p>See <a href="{@docRoot}guide/topics/resources/string-resource.html#Plurals">String
+ * Resources</a> for more on quantity strings.
+ *
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
@@ -312,6 +349,9 @@ public class Resources {
* Return the string value associated with a particular resource ID for a particular
* numerical quantity.
*
+ * <p>See <a href="{@docRoot}guide/topics/resources/string-resource.html#Plurals">String
+ * Resources</a> for more on quantity strings.
+ *
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
@@ -1334,7 +1374,7 @@ public class Resources {
}
synchronized (mSync) {
if (mPluralRule != null) {
- mPluralRule = PluralRules.ruleForLocale(config.locale);
+ mPluralRule = NativePluralRules.forLocale(config.locale);
}
}
}
diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java
index a5e5e46..9b14998 100644
--- a/core/java/android/database/AbstractCursor.java
+++ b/core/java/android/database/AbstractCursor.java
@@ -18,16 +18,11 @@ package android.database;
import android.content.ContentResolver;
import android.net.Uri;
+import android.os.Bundle;
import android.util.Config;
import android.util.Log;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
import java.lang.ref.WeakReference;
-import java.lang.UnsupportedOperationException;
import java.util.HashMap;
import java.util.Map;
@@ -56,6 +51,10 @@ public abstract class AbstractCursor implements CrossProcessCursor {
abstract public double getDouble(int column);
abstract public boolean isNull(int column);
+ public int getType(int column) {
+ throw new UnsupportedOperationException();
+ }
+
// TODO implement getBlob in all cursor types
public byte[] getBlob(int column) {
throw new UnsupportedOperationException("getBlob is not supported");
@@ -88,7 +87,7 @@ public abstract class AbstractCursor implements CrossProcessCursor {
}
mDataSetObservable.notifyInvalidated();
}
-
+
public boolean requery() {
if (mSelfObserver != null && mSelfObserverRegistered == false) {
mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
@@ -109,22 +108,6 @@ public abstract class AbstractCursor implements CrossProcessCursor {
}
/**
- * @hide
- * @deprecated
- */
- public boolean commitUpdates(Map<? extends Long,? extends Map<String,Object>> values) {
- return false;
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean deleteRow() {
- return false;
- }
-
- /**
* This function is called every time the cursor is successfully scrolled
* to a new position, giving the subclass a chance to update any state it
* may have. If it returns false the move function will also do so and the
@@ -320,137 +303,6 @@ public abstract class AbstractCursor implements CrossProcessCursor {
return getColumnNames()[columnIndex];
}
- /**
- * @hide
- * @deprecated
- */
- public boolean updateBlob(int columnIndex, byte[] value) {
- return update(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateString(int columnIndex, String value) {
- return update(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateShort(int columnIndex, short value) {
- return update(columnIndex, Short.valueOf(value));
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateInt(int columnIndex, int value) {
- return update(columnIndex, Integer.valueOf(value));
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateLong(int columnIndex, long value) {
- return update(columnIndex, Long.valueOf(value));
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateFloat(int columnIndex, float value) {
- return update(columnIndex, Float.valueOf(value));
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateDouble(int columnIndex, double value) {
- return update(columnIndex, Double.valueOf(value));
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateToNull(int columnIndex) {
- return update(columnIndex, null);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean update(int columnIndex, Object obj) {
- if (!supportsUpdates()) {
- return false;
- }
-
- // Long.valueOf() returns null sometimes!
-// Long rowid = Long.valueOf(getLong(mRowIdColumnIndex));
- Long rowid = new Long(getLong(mRowIdColumnIndex));
- if (rowid == null) {
- throw new IllegalStateException("null rowid. mRowIdColumnIndex = " + mRowIdColumnIndex);
- }
-
- synchronized(mUpdatedRows) {
- Map<String, Object> row = mUpdatedRows.get(rowid);
- if (row == null) {
- row = new HashMap<String, Object>();
- mUpdatedRows.put(rowid, row);
- }
- row.put(getColumnNames()[columnIndex], obj);
- }
-
- return true;
- }
-
- /**
- * Returns <code>true</code> if there are pending updates that have not yet been committed.
- *
- * @return <code>true</code> if there are pending updates that have not yet been committed.
- * @hide
- * @deprecated
- */
- public boolean hasUpdates() {
- synchronized(mUpdatedRows) {
- return mUpdatedRows.size() > 0;
- }
- }
-
- /**
- * @hide
- * @deprecated
- */
- public void abortUpdates() {
- synchronized(mUpdatedRows) {
- mUpdatedRows.clear();
- }
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean commitUpdates() {
- return commitUpdates(null);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean supportsUpdates() {
- return mRowIdColumnIndex != -1;
- }
-
public void registerContentObserver(ContentObserver observer) {
mContentObservable.registerObserver(observer);
}
@@ -478,9 +330,9 @@ public abstract class AbstractCursor implements CrossProcessCursor {
return mDataSetObservable;
}
+
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
-
}
public void unregisterDataSetObserver(DataSetObserver observer) {
@@ -535,36 +387,19 @@ public abstract class AbstractCursor implements CrossProcessCursor {
}
/**
- * This function returns true if the field has been updated and is
- * used in conjunction with {@link #getUpdatedField} to allow subclasses to
- * support reading uncommitted updates. NOTE: This function and
- * {@link #getUpdatedField} should be called together inside of a
- * block synchronized on mUpdatedRows.
- *
- * @param columnIndex the column index of the field to check
- * @return true if the field has been updated, false otherwise
+ * @deprecated Always returns false since Cursors do not support updating rows
*/
+ @Deprecated
protected boolean isFieldUpdated(int columnIndex) {
- if (mRowIdColumnIndex != -1 && mUpdatedRows.size() > 0) {
- Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID);
- if (updates != null && updates.containsKey(getColumnNames()[columnIndex])) {
- return true;
- }
- }
return false;
}
/**
- * This function returns the uncommitted updated value for the field
- * at columnIndex. NOTE: This function and {@link #isFieldUpdated} should
- * be called together inside of a block synchronized on mUpdatedRows.
- *
- * @param columnIndex the column index of the field to retrieve
- * @return the updated value
+ * @deprecated Always returns null since Cursors do not support updating rows
*/
+ @Deprecated
protected Object getUpdatedField(int columnIndex) {
- Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID);
- return updates.get(getColumnNames()[columnIndex]);
+ return null;
}
/**
@@ -614,11 +449,9 @@ public abstract class AbstractCursor implements CrossProcessCursor {
}
/**
- * This HashMap contains a mapping from Long rowIDs to another Map
- * that maps from String column names to new values. A NULL value means to
- * remove an existing value, and all numeric values are in their class
- * forms, i.e. Integer, Long, Float, etc.
+ * @deprecated This is never updated by this class and should not be used
*/
+ @Deprecated
protected HashMap<Long, Map<String, Object>> mUpdatedRows;
/**
@@ -628,6 +461,11 @@ public abstract class AbstractCursor implements CrossProcessCursor {
protected int mRowIdColumnIndex;
protected int mPos;
+ /**
+ * If {@link #mRowIdColumnIndex} is not -1 this contains contains the value of
+ * the column at {@link #mRowIdColumnIndex} for the current row this cursor is
+ * pointing at.
+ */
protected Long mCurrentRowID;
protected ContentResolver mContentResolver;
protected boolean mClosed = false;
diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java
index 27a02e2..8addaa8 100644
--- a/core/java/android/database/AbstractWindowedCursor.java
+++ b/core/java/android/database/AbstractWindowedCursor.java
@@ -19,202 +19,105 @@ package android.database;
/**
* A base class for Cursors that store their data in {@link CursorWindow}s.
*/
-public abstract class AbstractWindowedCursor extends AbstractCursor
-{
+public abstract class AbstractWindowedCursor extends AbstractCursor {
@Override
- public byte[] getBlob(int columnIndex)
- {
+ public byte[] getBlob(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- return (byte[])getUpdatedField(columnIndex);
- }
- }
-
return mWindow.getBlob(mPos, columnIndex);
}
@Override
- public String getString(int columnIndex)
- {
+ public String getString(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- return (String)getUpdatedField(columnIndex);
- }
- }
-
return mWindow.getString(mPos, columnIndex);
}
-
+
@Override
- public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)
- {
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- super.copyStringToBuffer(columnIndex, buffer);
- }
- }
-
mWindow.copyStringToBuffer(mPos, columnIndex, buffer);
}
@Override
- public short getShort(int columnIndex)
- {
+ public short getShort(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Number value = (Number)getUpdatedField(columnIndex);
- return value.shortValue();
- }
- }
-
return mWindow.getShort(mPos, columnIndex);
}
@Override
- public int getInt(int columnIndex)
- {
+ public int getInt(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Number value = (Number)getUpdatedField(columnIndex);
- return value.intValue();
- }
- }
-
return mWindow.getInt(mPos, columnIndex);
}
@Override
- public long getLong(int columnIndex)
- {
+ public long getLong(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Number value = (Number)getUpdatedField(columnIndex);
- return value.longValue();
- }
- }
-
return mWindow.getLong(mPos, columnIndex);
}
@Override
- public float getFloat(int columnIndex)
- {
+ public float getFloat(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Number value = (Number)getUpdatedField(columnIndex);
- return value.floatValue();
- }
- }
-
return mWindow.getFloat(mPos, columnIndex);
}
@Override
- public double getDouble(int columnIndex)
- {
+ public double getDouble(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Number value = (Number)getUpdatedField(columnIndex);
- return value.doubleValue();
- }
- }
-
return mWindow.getDouble(mPos, columnIndex);
}
@Override
- public boolean isNull(int columnIndex)
- {
+ public boolean isNull(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- return getUpdatedField(columnIndex) == null;
- }
- }
-
- return mWindow.isNull(mPos, columnIndex);
+ return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL;
}
- public boolean isBlob(int columnIndex)
- {
- checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Object object = getUpdatedField(columnIndex);
- return object == null || object instanceof byte[];
- }
- }
-
- return mWindow.isBlob(mPos, columnIndex);
+ /**
+ * @deprecated Use {@link #getType}
+ */
+ @Deprecated
+ public boolean isBlob(int columnIndex) {
+ return getType(columnIndex) == Cursor.FIELD_TYPE_BLOB;
}
- public boolean isString(int columnIndex)
- {
- checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Object object = getUpdatedField(columnIndex);
- return object == null || object instanceof String;
- }
- }
-
- return mWindow.isString(mPos, columnIndex);
+ /**
+ * @deprecated Use {@link #getType}
+ */
+ @Deprecated
+ public boolean isString(int columnIndex) {
+ return getType(columnIndex) == Cursor.FIELD_TYPE_STRING;
}
- public boolean isLong(int columnIndex)
- {
- checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Object object = getUpdatedField(columnIndex);
- return object != null && (object instanceof Integer || object instanceof Long);
- }
- }
+ /**
+ * @deprecated Use {@link #getType}
+ */
+ @Deprecated
+ public boolean isLong(int columnIndex) {
+ return getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER;
+ }
- return mWindow.isLong(mPos, columnIndex);
+ /**
+ * @deprecated Use {@link #getType}
+ */
+ @Deprecated
+ public boolean isFloat(int columnIndex) {
+ return getType(columnIndex) == Cursor.FIELD_TYPE_FLOAT;
}
- public boolean isFloat(int columnIndex)
- {
+ @Override
+ public int getType(int columnIndex) {
checkPosition();
-
- synchronized(mUpdatedRows) {
- if (isFieldUpdated(columnIndex)) {
- Object object = getUpdatedField(columnIndex);
- return object != null && (object instanceof Float || object instanceof Double);
- }
- }
-
- return mWindow.isFloat(mPos, columnIndex);
+ return mWindow.getType(mPos, columnIndex);
}
@Override
- protected void checkPosition()
- {
+ protected void checkPosition() {
super.checkPosition();
if (mWindow == null) {
- throw new StaleDataException("Access closed cursor");
+ throw new StaleDataException("Attempting to access a closed cursor");
}
}
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java
index baa94d8..fa62d69 100644
--- a/core/java/android/database/BulkCursorNative.java
+++ b/core/java/android/database/BulkCursorNative.java
@@ -17,13 +17,10 @@
package android.database;
import android.os.Binder;
-import android.os.RemoteException;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
-import android.os.Bundle;
-
-import java.util.HashMap;
-import java.util.Map;
+import android.os.RemoteException;
/**
* Native implementation of the bulk cursor. This is only for use in implementing
@@ -120,26 +117,6 @@ public abstract class BulkCursorNative extends Binder implements IBulkCursor
return true;
}
- case UPDATE_ROWS_TRANSACTION: {
- data.enforceInterface(IBulkCursor.descriptor);
- // TODO - what ClassLoader should be passed to readHashMap?
- // TODO - switch to Bundle
- HashMap<Long, Map<String, Object>> values = data.readHashMap(null);
- boolean result = updateRows(values);
- reply.writeNoException();
- reply.writeInt((result == true ? 1 : 0));
- return true;
- }
-
- case DELETE_ROW_TRANSACTION: {
- data.enforceInterface(IBulkCursor.descriptor);
- int position = data.readInt();
- boolean result = deleteRow(position);
- reply.writeNoException();
- reply.writeInt((result == true ? 1 : 0));
- return true;
- }
-
case ON_MOVE_TRANSACTION: {
data.enforceInterface(IBulkCursor.descriptor);
int position = data.readInt();
@@ -343,48 +320,6 @@ final class BulkCursorProxy implements IBulkCursor {
return count;
}
- public boolean updateRows(Map values) throws RemoteException
- {
- Parcel data = Parcel.obtain();
- Parcel reply = Parcel.obtain();
-
- data.writeInterfaceToken(IBulkCursor.descriptor);
-
- data.writeMap(values);
-
- mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0);
-
- DatabaseUtils.readExceptionFromParcel(reply);
-
- boolean result = (reply.readInt() == 1 ? true : false);
-
- data.recycle();
- reply.recycle();
-
- return result;
- }
-
- public boolean deleteRow(int position) throws RemoteException
- {
- Parcel data = Parcel.obtain();
- Parcel reply = Parcel.obtain();
-
- data.writeInterfaceToken(IBulkCursor.descriptor);
-
- data.writeInt(position);
-
- mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0);
-
- DatabaseUtils.readExceptionFromParcel(reply);
-
- boolean result = (reply.readInt() == 1 ? true : false);
-
- data.recycle();
- reply.recycle();
-
- return result;
- }
-
public boolean getWantsAllOnMoveCalls() throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java
index 1469ea2..2cb2aec 100644
--- a/core/java/android/database/BulkCursorToCursorAdaptor.java
+++ b/core/java/android/database/BulkCursorToCursorAdaptor.java
@@ -16,12 +16,10 @@
package android.database;
-import android.os.RemoteException;
import android.os.Bundle;
+import android.os.RemoteException;
import android.util.Log;
-import java.util.Map;
-
/**
* Adapts an {@link IBulkCursor} to a {@link Cursor} for use in the local
* process.
@@ -174,38 +172,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor {
}
}
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean deleteRow() {
- try {
- boolean result = mBulkCursor.deleteRow(mPos);
- if (result != false) {
- // The window contains the old value, discard it
- mWindow = null;
-
- // Fix up the position
- mCount = mBulkCursor.count();
- if (mPos < mCount) {
- int oldPos = mPos;
- mPos = -1;
- moveToPosition(oldPos);
- } else {
- mPos = mCount;
- }
-
- // Send the change notification
- onChange(true);
- }
- return result;
- } catch (RemoteException ex) {
- Log.e(TAG, "Unable to delete row because the remote process is dead");
- return false;
- }
- }
-
@Override
public String[] getColumnNames() {
if (mColumns == null) {
@@ -219,44 +185,6 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor {
return mColumns;
}
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean commitUpdates(Map<? extends Long,
- ? extends Map<String,Object>> additionalValues) {
- if (!supportsUpdates()) {
- Log.e(TAG, "commitUpdates not supported on this cursor, did you include the _id column?");
- return false;
- }
-
- synchronized(mUpdatedRows) {
- if (additionalValues != null) {
- mUpdatedRows.putAll(additionalValues);
- }
-
- if (mUpdatedRows.size() <= 0) {
- return false;
- }
-
- try {
- boolean result = mBulkCursor.updateRows(mUpdatedRows);
-
- if (result == true) {
- mUpdatedRows.clear();
-
- // Send the change notification
- onChange(true);
- }
- return result;
- } catch (RemoteException ex) {
- Log.e(TAG, "Unable to commit updates because the remote process is dead");
- return false;
- }
- }
- }
-
@Override
public Bundle getExtras() {
try {
diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java
index 6539156..c03c586 100644
--- a/core/java/android/database/Cursor.java
+++ b/core/java/android/database/Cursor.java
@@ -30,6 +30,25 @@ import java.util.Map;
* threads should perform its own synchronization when using the Cursor.
*/
public interface Cursor {
+ /*
+ * Values returned by {@link #getType(int)}.
+ * These should be consistent with the corresponding types defined in CursorWindow.h
+ */
+ /** Value returned by {@link #getType(int)} if the specified column is null */
+ static final int FIELD_TYPE_NULL = 0;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is integer */
+ static final int FIELD_TYPE_INTEGER = 1;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is float */
+ static final int FIELD_TYPE_FLOAT = 2;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is string */
+ static final int FIELD_TYPE_STRING = 3;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is blob */
+ static final int FIELD_TYPE_BLOB = 4;
+
/**
* Returns the numbers of rows in the cursor.
*
@@ -146,22 +165,6 @@ public interface Cursor {
boolean isAfterLast();
/**
- * Removes the row at the current cursor position from the underlying data
- * store. After this method returns the cursor will be pointing to the row
- * after the row that is deleted. This has the side effect of decrementing
- * the result of count() by one.
- * <p>
- * The query must have the row ID column in its selection, otherwise this
- * call will fail.
- *
- * @hide
- * @return whether the record was successfully deleted.
- * @deprecated use {@link ContentResolver#delete(Uri, String, String[])}
- */
- @Deprecated
- boolean deleteRow();
-
- /**
* Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
* If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which
* will make the error more clear.
@@ -295,194 +298,33 @@ public interface Cursor {
double getDouble(int columnIndex);
/**
- * Returns <code>true</code> if the value in the indicated column is null.
- *
- * @param columnIndex the zero-based index of the target column.
- * @return whether the column value is null.
- */
- boolean isNull(int columnIndex);
-
- /**
- * Returns <code>true</code> if the cursor supports updates.
- *
- * @return whether the cursor supports updates.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean supportsUpdates();
-
- /**
- * Returns <code>true</code> if there are pending updates that have not yet been committed.
- *
- * @return <code>true</code> if there are pending updates that have not yet been committed.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean hasUpdates();
-
- /**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
- *
- * @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateBlob(int columnIndex, byte[] value);
-
- /**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
- *
- * @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateString(int columnIndex, String value);
-
- /**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
+ * Returns data type of the given column's value.
+ * The preferred type of the column is returned but the data may be converted to other types
+ * as documented in the get-type methods such as {@link #getInt(int)}, {@link #getFloat(int)}
+ * etc.
+ *<p>
+ * Returned column types are
+ * <ul>
+ * <li>{@link #FIELD_TYPE_NULL}</li>
+ * <li>{@link #FIELD_TYPE_INTEGER}</li>
+ * <li>{@link #FIELD_TYPE_FLOAT}</li>
+ * <li>{@link #FIELD_TYPE_STRING}</li>
+ * <li>{@link #FIELD_TYPE_BLOB}</li>
+ *</ul>
+ *</p>
*
* @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
+ * @return column value type
*/
- @Deprecated
- boolean updateShort(int columnIndex, short value);
+ int getType(int columnIndex);
/**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
- *
- * @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateInt(int columnIndex, int value);
-
- /**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
- *
- * @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateLong(int columnIndex, long value);
-
- /**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
- *
- * @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateFloat(int columnIndex, float value);
-
- /**
- * Updates the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
- *
- * @param columnIndex the zero-based index of the target column.
- * @param value the new value.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateDouble(int columnIndex, double value);
-
- /**
- * Removes the value for the given column in the row the cursor is
- * currently pointing at. Updates are not committed to the backing store
- * until {@link #commitUpdates()} is called.
+ * Returns <code>true</code> if the value in the indicated column is null.
*
* @param columnIndex the zero-based index of the target column.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean updateToNull(int columnIndex);
-
- /**
- * Atomically commits all updates to the backing store. After completion,
- * this method leaves the data in an inconsistent state and you should call
- * {@link #requery} before reading data from the cursor again.
- *
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean commitUpdates();
-
- /**
- * Atomically commits all updates to the backing store, as well as the
- * updates included in values. After completion,
- * this method leaves the data in an inconsistent state and you should call
- * {@link #requery} before reading data from the cursor again.
- *
- * @param values A map from row IDs to Maps associating column names with
- * updated values. A null value indicates the field should be
- removed.
- * @return whether the operation succeeded.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
- */
- @Deprecated
- boolean commitUpdates(Map<? extends Long,
- ? extends Map<String,Object>> values);
-
- /**
- * Reverts all updates made to the cursor since the last call to
- * commitUpdates.
- * @hide
- * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
- * update methods
+ * @return whether the column value is null.
*/
- @Deprecated
- void abortUpdates();
+ boolean isNull(int columnIndex);
/**
* Deactivates the Cursor, making all calls on it fail until {@link #requery} is called.
@@ -496,6 +338,10 @@ public interface Cursor {
* contents. This may be done at any time, including after a call to {@link
* #deactivate}.
*
+ * Since this method could execute a query on the database and potentially take
+ * a while, it could cause ANR if it is called on Main (UI) thread.
+ * A warning is printed if this method is being executed on Main thread.
+ *
* @return true if the requery succeeded, false if not, in which case the
* cursor becomes invalid.
*/
diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java
index 748eb99..8bc7de2 100644
--- a/core/java/android/database/CursorToBulkCursorAdaptor.java
+++ b/core/java/android/database/CursorToBulkCursorAdaptor.java
@@ -16,16 +16,12 @@
package android.database;
-import android.database.sqlite.SQLiteMisuseException;
-import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Config;
import android.util.Log;
-import java.util.Map;
-
/**
* Wraps a BulkCursor around an existing Cursor making it remotable.
@@ -38,7 +34,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
private final CrossProcessCursor mCursor;
private CursorWindow mWindow;
private final String mProviderName;
- private final boolean mReadOnly;
private ContentObserverProxy mObserver;
private static final class ContentObserverProxy extends ContentObserver
@@ -98,7 +93,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
"Only CrossProcessCursor cursors are supported across process for now", e);
}
mProviderName = providerName;
- mReadOnly = !allowWrite;
createAndRegisterObserverProxy(observer);
}
@@ -197,31 +191,6 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
}
}
- public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) {
- if (mReadOnly) {
- Log.w("ContentProvider", "Permission Denial: modifying "
- + mProviderName
- + " from pid=" + Binder.getCallingPid()
- + ", uid=" + Binder.getCallingUid());
- return false;
- }
- return mCursor.commitUpdates(values);
- }
-
- public boolean deleteRow(int position) {
- if (mReadOnly) {
- Log.w("ContentProvider", "Permission Denial: modifying "
- + mProviderName
- + " from pid=" + Binder.getCallingPid()
- + ", uid=" + Binder.getCallingUid());
- return false;
- }
- if (mCursor.moveToPosition(position) == false) {
- return false;
- }
- return mCursor.deleteRow();
- }
-
public Bundle getExtras() {
return mCursor.getExtras();
}
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index c756825..599431f 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -217,18 +217,13 @@ public class CursorWindow extends SQLiteClosable implements Parcelable {
* @param row the row to read from, row - getStartPosition() being the actual row in the window
* @param col the column to read from
* @return {@code true} if given field is {@code NULL}
+ * @deprecated use {@link #getType(int, int)} instead
*/
+ @Deprecated
public boolean isNull(int row, int col) {
- acquireReference();
- try {
- return isNull_native(row - mStartPos, col);
- } finally {
- releaseReference();
- }
+ return getType(row, col) == Cursor.FIELD_TYPE_NULL;
}
- private native boolean isNull_native(int row, int col);
-
/**
* Returns a byte array for the given field.
*
@@ -248,35 +243,56 @@ public class CursorWindow extends SQLiteClosable implements Parcelable {
private native byte[] getBlob_native(int row, int col);
/**
- * Checks if a field contains either a blob or is null.
+ * Returns data type of the given column's value.
+ *<p>
+ * Returned column types are
+ * <ul>
+ * <li>{@link Cursor#FIELD_TYPE_NULL}</li>
+ * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
+ * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
+ * <li>{@link Cursor#FIELD_TYPE_STRING}</li>
+ * <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
+ *</ul>
+ *</p>
*
* @param row the row to read from, row - getStartPosition() being the actual row in the window
* @param col the column to read from
- * @return {@code true} if given field is {@code NULL} or a blob
+ * @return the value type
*/
- public boolean isBlob(int row, int col) {
+ public int getType(int row, int col) {
acquireReference();
try {
- return isBlob_native(row - mStartPos, col);
+ return getType_native(row - mStartPos, col);
} finally {
releaseReference();
}
}
/**
+ * Checks if a field contains either a blob or is null.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return {@code true} if given field is {@code NULL} or a blob
+ * @deprecated use {@link #getType(int, int)} instead
+ */
+ @Deprecated
+ public boolean isBlob(int row, int col) {
+ int type = getType(row, col);
+ return type == Cursor.FIELD_TYPE_BLOB || type == Cursor.FIELD_TYPE_NULL;
+ }
+
+ /**
* Checks if a field contains a long
*
* @param row the row to read from, row - getStartPosition() being the actual row in the window
* @param col the column to read from
* @return {@code true} if given field is a long
+ * @deprecated use {@link #getType(int, int)} instead
*/
+ @Deprecated
public boolean isLong(int row, int col) {
- acquireReference();
- try {
- return isInteger_native(row - mStartPos, col);
- } finally {
- releaseReference();
- }
+ return getType(row, col) == Cursor.FIELD_TYPE_INTEGER;
}
/**
@@ -285,14 +301,11 @@ public class CursorWindow extends SQLiteClosable implements Parcelable {
* @param row the row to read from, row - getStartPosition() being the actual row in the window
* @param col the column to read from
* @return {@code true} if given field is a float
+ * @deprecated use {@link #getType(int, int)} instead
*/
+ @Deprecated
public boolean isFloat(int row, int col) {
- acquireReference();
- try {
- return isFloat_native(row - mStartPos, col);
- } finally {
- releaseReference();
- }
+ return getType(row, col) == Cursor.FIELD_TYPE_FLOAT;
}
/**
@@ -301,20 +314,15 @@ public class CursorWindow extends SQLiteClosable implements Parcelable {
* @param row the row to read from, row - getStartPosition() being the actual row in the window
* @param col the column to read from
* @return {@code true} if given field is {@code NULL} or a String
+ * @deprecated use {@link #getType(int, int)} instead
*/
+ @Deprecated
public boolean isString(int row, int col) {
- acquireReference();
- try {
- return isString_native(row - mStartPos, col);
- } finally {
- releaseReference();
- }
+ int type = getType(row, col);
+ return type == Cursor.FIELD_TYPE_STRING || type == Cursor.FIELD_TYPE_NULL;
}
- private native boolean isBlob_native(int row, int col);
- private native boolean isString_native(int row, int col);
- private native boolean isInteger_native(int row, int col);
- private native boolean isFloat_native(int row, int col);
+ private native int getType_native(int row, int col);
/**
* Returns a String for the given field.
diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java
index f0aa7d7..3c3bd43 100644
--- a/core/java/android/database/CursorWrapper.java
+++ b/core/java/android/database/CursorWrapper.java
@@ -17,28 +17,26 @@
package android.database;
import android.content.ContentResolver;
-import android.database.CharArrayBuffer;
import android.net.Uri;
import android.os.Bundle;
-import java.util.Map;
-
/**
- * Wrapper class for Cursor that delegates all calls to the actual cursor object
+ * Wrapper class for Cursor that delegates all calls to the actual cursor object. The primary
+ * use for this class is to extend a cursor while overriding only a subset of its methods.
*/
-
public class CursorWrapper implements Cursor {
+ private final Cursor mCursor;
+
public CursorWrapper(Cursor cursor) {
mCursor = cursor;
}
-
+
/**
- * @hide
- * @deprecated
+ * @return the wrapped cursor
*/
- public void abortUpdates() {
- mCursor.abortUpdates();
+ public Cursor getWrappedCursor() {
+ return mCursor;
}
public void close() {
@@ -49,23 +47,6 @@ public class CursorWrapper implements Cursor {
return mCursor.isClosed();
}
- /**
- * @hide
- * @deprecated
- */
- public boolean commitUpdates() {
- return mCursor.commitUpdates();
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean commitUpdates(
- Map<? extends Long, ? extends Map<String, Object>> values) {
- return mCursor.commitUpdates(values);
- }
-
public int getCount() {
return mCursor.getCount();
}
@@ -74,14 +55,6 @@ public class CursorWrapper implements Cursor {
mCursor.deactivate();
}
- /**
- * @hide
- * @deprecated
- */
- public boolean deleteRow() {
- return mCursor.deleteRow();
- }
-
public boolean moveToFirst() {
return mCursor.moveToFirst();
}
@@ -147,14 +120,6 @@ public class CursorWrapper implements Cursor {
return mCursor.getWantsAllOnMoveCalls();
}
- /**
- * @hide
- * @deprecated
- */
- public boolean hasUpdates() {
- return mCursor.hasUpdates();
- }
-
public boolean isAfterLast() {
return mCursor.isAfterLast();
}
@@ -171,6 +136,10 @@ public class CursorWrapper implements Cursor {
return mCursor.isLast();
}
+ public int getType(int columnIndex) {
+ return mCursor.getType(columnIndex);
+ }
+
public boolean isNull(int columnIndex) {
return mCursor.isNull(columnIndex);
}
@@ -219,14 +188,6 @@ public class CursorWrapper implements Cursor {
mCursor.setNotificationUri(cr, uri);
}
- /**
- * @hide
- * @deprecated
- */
- public boolean supportsUpdates() {
- return mCursor.supportsUpdates();
- }
-
public void unregisterContentObserver(ContentObserver observer) {
mCursor.unregisterContentObserver(observer);
}
@@ -234,72 +195,5 @@ public class CursorWrapper implements Cursor {
public void unregisterDataSetObserver(DataSetObserver observer) {
mCursor.unregisterDataSetObserver(observer);
}
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateDouble(int columnIndex, double value) {
- return mCursor.updateDouble(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateFloat(int columnIndex, float value) {
- return mCursor.updateFloat(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateInt(int columnIndex, int value) {
- return mCursor.updateInt(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateLong(int columnIndex, long value) {
- return mCursor.updateLong(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateShort(int columnIndex, short value) {
- return mCursor.updateShort(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateString(int columnIndex, String value) {
- return mCursor.updateString(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateBlob(int columnIndex, byte[] value) {
- return mCursor.updateBlob(columnIndex, value);
- }
-
- /**
- * @hide
- * @deprecated
- */
- public boolean updateToNull(int columnIndex) {
- return mCursor.updateToNull(columnIndex);
- }
-
- private Cursor mCursor;
-
}
diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java
index 9200e81..51c72c1 100644
--- a/core/java/android/database/DataSetObservable.java
+++ b/core/java/android/database/DataSetObservable.java
@@ -27,8 +27,12 @@ public class DataSetObservable extends Observable<DataSetObserver> {
*/
public void notifyChanged() {
synchronized(mObservers) {
- for (DataSetObserver observer : mObservers) {
- observer.onChanged();
+ // since onChanged() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onChanged();
}
}
}
@@ -39,8 +43,8 @@ public class DataSetObservable extends Observable<DataSetObserver> {
*/
public void notifyInvalidated() {
synchronized (mObservers) {
- for (DataSetObserver observer : mObservers) {
- observer.onInvalidated();
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onInvalidated();
}
}
}
diff --git a/core/java/android/pim/vcard/exception/VCardException.java b/core/java/android/database/DatabaseErrorHandler.java
index e557219..f0c5452 100644
--- a/core/java/android/pim/vcard/exception/VCardException.java
+++ b/core/java/android/database/DatabaseErrorHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,23 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.pim.vcard.exception;
-public class VCardException extends java.lang.Exception {
- /**
- * Constructs a VCardException object
- */
- public VCardException() {
- super();
- }
+package android.database;
+
+import android.database.sqlite.SQLiteDatabase;
+
+/**
+ * An interface to let the apps define the actions to take when the following errors are detected
+ * database corruption
+ */
+public interface DatabaseErrorHandler {
/**
- * Constructs a VCardException object
- *
- * @param message the error message
+ * defines the method to be invoked when database corruption is detected.
+ * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+ * is detected.
*/
- public VCardException(String message) {
- super(message);
- }
-
+ void onCorruption(SQLiteDatabase dbObj);
}
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
index 9bfbb74..af93eee 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -193,6 +193,37 @@ public class DatabaseUtils {
}
/**
+ * Returns data type of the given object's value.
+ *<p>
+ * Returned values are
+ * <ul>
+ * <li>{@link Cursor#FIELD_TYPE_NULL}</li>
+ * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
+ * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
+ * <li>{@link Cursor#FIELD_TYPE_STRING}</li>
+ * <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
+ *</ul>
+ *</p>
+ *
+ * @param obj the object whose value type is to be returned
+ * @return object value type
+ * @hide
+ */
+ public static int getTypeOfObject(Object obj) {
+ if (obj == null) {
+ return Cursor.FIELD_TYPE_NULL;
+ } else if (obj instanceof byte[]) {
+ return Cursor.FIELD_TYPE_BLOB;
+ } else if (obj instanceof Float || obj instanceof Double) {
+ return Cursor.FIELD_TYPE_FLOAT;
+ } else if (obj instanceof Long || obj instanceof Integer) {
+ return Cursor.FIELD_TYPE_INTEGER;
+ } else {
+ return Cursor.FIELD_TYPE_STRING;
+ }
+ }
+
+ /**
* Appends an SQL string to the given StringBuilder, including the opening
* and closing single quotes. Any single quotes internal to sqlString will
* be escaped.
diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java
new file mode 100644
index 0000000..3619e48
--- /dev/null
+++ b/core/java/android/database/DefaultDatabaseErrorHandler.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.database;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+
+/**
+ * Default class used defining the actions to take when the following errors are detected
+ * database corruption
+ */
+public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler {
+
+ private static final String TAG = "DefaultDatabaseErrorHandler";
+
+ /**
+ * defines the default method to be invoked when database corruption is detected.
+ * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+ * is detected.
+ */
+ public void onCorruption(SQLiteDatabase dbObj) {
+ Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath());
+
+ // is the corruption detected even before database could be 'opened'?
+ if (!dbObj.isOpen()) {
+ // database files are not even openable. delete this database file.
+ // NOTE if the database has attached databases, then any of them could be corrupt.
+ // and not deleting all of them could cause corrupted database file to remain and
+ // make the application crash on database open operation. To avoid this problem,
+ // the application should provide its own {@link DatabaseErrorHandler} impl class
+ // to delete ALL files of the database (including the attached databases).
+ deleteDatabaseFile(dbObj.getPath());
+ return;
+ }
+
+ ArrayList<Pair<String, String>> attachedDbs = null;
+ try {
+ // Close the database, which will cause subsequent operations to fail.
+ // before that, get the attached database list first.
+ try {
+ attachedDbs = dbObj.getAttachedDbs();
+ } catch (SQLiteException e) {
+ /* ignore */
+ }
+ try {
+ dbObj.close();
+ } catch (SQLiteException e) {
+ /* ignore */
+ }
+ } finally {
+ // Delete all files of this corrupt database and/or attached databases
+ if (attachedDbs != null) {
+ for (Pair<String, String> p : attachedDbs) {
+ deleteDatabaseFile(p.second);
+ }
+ } else {
+ // attachedDbs = null is possible when the database is so corrupt that even
+ // "PRAGMA database_list;" also fails. delete the main database file
+ deleteDatabaseFile(dbObj.getPath());
+ }
+ }
+ }
+
+ private void deleteDatabaseFile(String fileName) {
+ if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
+ return;
+ }
+ Log.e(TAG, "deleting the database file: " + fileName);
+ try {
+ new File(fileName).delete();
+ } catch (Exception e) {
+ /* print warning and ignore exception */
+ Log.w(TAG, "delete failed: " + e.getMessage());
+ }
+ }
+}
diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java
index 46790a3..244c88f 100644
--- a/core/java/android/database/IBulkCursor.java
+++ b/core/java/android/database/IBulkCursor.java
@@ -16,16 +16,14 @@
package android.database;
-import android.os.RemoteException;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
-import android.os.Bundle;
-
-import java.util.Map;
+import android.os.RemoteException;
/**
* This interface provides a low-level way to pass bulk cursor data across
- * both process and language boundries. Application code should use the Cursor
+ * both process and language boundaries. Application code should use the Cursor
* interface directly.
*
* {@hide}
@@ -54,10 +52,6 @@ public interface IBulkCursor extends IInterface {
*/
public String[] getColumnNames() throws RemoteException;
- public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) throws RemoteException;
-
- public boolean deleteRow(int position) throws RemoteException;
-
public void deactivate() throws RemoteException;
public void close() throws RemoteException;
@@ -76,8 +70,6 @@ public interface IBulkCursor extends IInterface {
static final int GET_CURSOR_WINDOW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
static final int COUNT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
static final int GET_COLUMN_NAMES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
- static final int UPDATE_ROWS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
- static final int DELETE_ROW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4;
static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5;
static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6;
static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 7;
diff --git a/core/java/android/database/MatrixCursor.java b/core/java/android/database/MatrixCursor.java
index d5c3a32..5c1b968 100644
--- a/core/java/android/database/MatrixCursor.java
+++ b/core/java/android/database/MatrixCursor.java
@@ -272,6 +272,11 @@ public class MatrixCursor extends AbstractCursor {
}
@Override
+ public int getType(int column) {
+ return DatabaseUtils.getTypeOfObject(get(column));
+ }
+
+ @Override
public boolean isNull(int column) {
return get(column) == null;
}
diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java
index 722d707..2c25db7 100644
--- a/core/java/android/database/MergeCursor.java
+++ b/core/java/android/database/MergeCursor.java
@@ -92,32 +92,6 @@ public class MergeCursor extends AbstractCursor
return false;
}
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean deleteRow()
- {
- return mCursor.deleteRow();
- }
-
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean commitUpdates() {
- int length = mCursors.length;
- for (int i = 0 ; i < length ; i++) {
- if (mCursors[i] != null) {
- mCursors[i].commitUpdates();
- }
- }
- onChange(true);
- return true;
- }
-
@Override
public String getString(int column)
{
@@ -155,6 +129,11 @@ public class MergeCursor extends AbstractCursor
}
@Override
+ public int getType(int column) {
+ return mCursor.getType(column);
+ }
+
+ @Override
public boolean isNull(int column)
{
return mCursor.isNull(column);
diff --git a/core/java/android/pim/vcard/exception/VCardVersionException.java b/core/java/android/database/RequeryOnUiThreadException.java
index 9fe8b7f..97a50d8 100644
--- a/core/java/android/pim/vcard/exception/VCardVersionException.java
+++ b/core/java/android/database/RequeryOnUiThreadException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 The Android Open Source Project
+ * 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.
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package android.pim.vcard.exception;
+package android.database;
/**
- * VCardException used only when the version of the vCard is different.
+ * An exception that indicates invoking {@link Cursor#requery()} on Main thread could cause ANR.
+ * This exception should encourage apps to invoke {@link Cursor#requery()} in a background thread.
+ * @hide
*/
-public class VCardVersionException extends VCardException {
- public VCardVersionException() {
- super();
- }
- public VCardVersionException(String message) {
- super(message);
+public class RequeryOnUiThreadException extends RuntimeException {
+ public RequeryOnUiThreadException(String packageName) {
+ super("In " + packageName + " Requery is executing on main (UI) thread. could cause ANR. " +
+ "do it in background thread.");
}
}
diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java
new file mode 100644
index 0000000..50b2919
--- /dev/null
+++ b/core/java/android/database/sqlite/DatabaseConnectionPool.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 20010 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.os.SystemClock;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Random;
+
+/**
+ * A connection pool to be used by readers.
+ * Note that each connection can be used by only one reader at a time.
+ */
+/* package */ class DatabaseConnectionPool {
+
+ private static final String TAG = "DatabaseConnectionPool";
+
+ /** The default connection pool size. It is set based on the amount of memory the device has.
+ * TODO: set this with 'a system call' which returns the amount of memory the device has
+ */
+ private static final int DEFAULT_CONNECTION_POOL_SIZE = 1;
+
+ /** the pool size set for this {@link SQLiteDatabase} */
+ private volatile int mMaxPoolSize = DEFAULT_CONNECTION_POOL_SIZE;
+
+ /** The connection pool objects are stored in this member.
+ * TODO: revisit this data struct as the number of pooled connections increase beyond
+ * single-digit values.
+ */
+ private final ArrayList<PoolObj> mPool = new ArrayList<PoolObj>(mMaxPoolSize);
+
+ /** the main database connection to which this connection pool is attached */
+ private final SQLiteDatabase mParentDbObj;
+
+ /** Random number generator used to pick a free connection out of the pool */
+ private Random rand; // lazily initialized
+
+ /* package */ DatabaseConnectionPool(SQLiteDatabase db) {
+ this.mParentDbObj = db;
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Max Pool Size: " + mMaxPoolSize);
+ }
+ }
+
+ /**
+ * close all database connections in the pool - even if they are in use!
+ */
+ /* package */ void close() {
+ synchronized(mParentDbObj) {
+ for (int i = mPool.size() - 1; i >= 0; i--) {
+ mPool.get(i).mDb.close();
+ }
+ mPool.clear();
+ }
+ }
+
+ /**
+ * get a free connection from the pool
+ *
+ * @param sql if not null, try to find a connection inthe pool which already has cached
+ * the compiled statement for this sql.
+ * @return the Database connection that the caller can use
+ */
+ /* package */ SQLiteDatabase get(String sql) {
+ SQLiteDatabase db = null;
+ PoolObj poolObj = null;
+ synchronized(mParentDbObj) {
+ int poolSize = mPool.size();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ assert sql != null;
+ doAsserts();
+ }
+ if (getFreePoolSize() == 0) {
+ // no free ( = available) connections
+ if (mMaxPoolSize == poolSize) {
+ // maxed out. can't open any more connections.
+ // let the caller wait on one of the pooled connections
+ // preferably a connection caching the pre-compiled statement of the given SQL
+ if (mMaxPoolSize == 1) {
+ poolObj = mPool.get(0);
+ } else {
+ for (int i = 0; i < mMaxPoolSize; i++) {
+ if (mPool.get(i).mDb.isSqlInStatementCache(sql)) {
+ poolObj = mPool.get(i);
+ break;
+ }
+ }
+ if (poolObj == null) {
+ // there are no database connections with the given SQL pre-compiled.
+ // ok to return any of the connections.
+ if (rand == null) {
+ rand = new Random(SystemClock.elapsedRealtime());
+ }
+ poolObj = mPool.get(rand.nextInt(mMaxPoolSize));
+ }
+ }
+ db = poolObj.mDb;
+ } else {
+ // create a new connection and add it to the pool, since we haven't reached
+ // max pool size allowed
+ db = mParentDbObj.createPoolConnection((short)(poolSize + 1));
+ poolObj = new PoolObj(db);
+ mPool.add(poolSize, poolObj);
+ }
+ } else {
+ // there are free connections available. pick one
+ // preferably a connection caching the pre-compiled statement of the given SQL
+ for (int i = 0; i < poolSize; i++) {
+ if (mPool.get(i).isFree() && mPool.get(i).mDb.isSqlInStatementCache(sql)) {
+ poolObj = mPool.get(i);
+ break;
+ }
+ }
+ if (poolObj == null) {
+ // didn't find a free database connection with the given SQL already
+ // pre-compiled. return a free connection (this means, the same SQL could be
+ // pre-compiled on more than one database connection. potential wasted memory.)
+ for (int i = 0; i < poolSize; i++) {
+ if (mPool.get(i).isFree()) {
+ poolObj = mPool.get(i);
+ break;
+ }
+ }
+ }
+ db = poolObj.mDb;
+ }
+
+ assert poolObj != null;
+ assert poolObj.mDb == db;
+
+ poolObj.acquire();
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "END get-connection: " + toString() + poolObj.toString());
+ }
+ return db;
+ // TODO if a thread acquires a connection and dies without releasing the connection, then
+ // there could be a connection leak.
+ }
+
+ /**
+ * release the given database connection back to the pool.
+ * @param db the connection to be released
+ */
+ /* package */ void release(SQLiteDatabase db) {
+ PoolObj poolObj;
+ synchronized(mParentDbObj) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ assert db.mConnectionNum > 0;
+ doAsserts();
+ assert mPool.get(db.mConnectionNum - 1).mDb == db;
+ }
+
+ poolObj = mPool.get(db.mConnectionNum - 1);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "BEGIN release-conn: " + toString() + poolObj.toString());
+ }
+
+ if (poolObj.isFree()) {
+ throw new IllegalStateException("Releasing object already freed: " +
+ db.mConnectionNum);
+ }
+
+ poolObj.release();
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "END release-conn: " + toString() + poolObj.toString());
+ }
+ }
+
+ /**
+ * Returns a list of all database connections in the pool (both free and busy connections).
+ * This method is used when "adb bugreport" is done.
+ */
+ /* package */ ArrayList<SQLiteDatabase> getConnectionList() {
+ ArrayList<SQLiteDatabase> list = new ArrayList<SQLiteDatabase>();
+ synchronized(mParentDbObj) {
+ for (int i = mPool.size() - 1; i >= 0; i--) {
+ list.add(mPool.get(i).mDb);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * package level access for testing purposes only. otherwise, private should be sufficient.
+ */
+ /* package */ int getFreePoolSize() {
+ int count = 0;
+ for (int i = mPool.size() - 1; i >= 0; i--) {
+ if (mPool.get(i).isFree()) {
+ count++;
+ }
+ }
+ return count++;
+ }
+
+ /**
+ * only for testing purposes
+ */
+ /* package */ ArrayList<PoolObj> getPool() {
+ return mPool;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buff = new StringBuilder();
+ buff.append("db: ");
+ buff.append(mParentDbObj.getPath());
+ buff.append(", totalsize = ");
+ buff.append(mPool.size());
+ buff.append(", #free = ");
+ buff.append(getFreePoolSize());
+ buff.append(", maxpoolsize = ");
+ buff.append(mMaxPoolSize);
+ for (PoolObj p : mPool) {
+ buff.append("\n");
+ buff.append(p.toString());
+ }
+ return buff.toString();
+ }
+
+ private void doAsserts() {
+ for (int i = 0; i < mPool.size(); i++) {
+ mPool.get(i).verify();
+ assert mPool.get(i).mDb.mConnectionNum == (i + 1);
+ }
+ }
+
+ /* package */ void setMaxPoolSize(int size) {
+ synchronized(mParentDbObj) {
+ mMaxPoolSize = size;
+ }
+ }
+
+ /* package */ int getMaxPoolSize() {
+ synchronized(mParentDbObj) {
+ return mMaxPoolSize;
+ }
+ }
+
+ /** only used for testing purposes. */
+ /* package */ boolean isDatabaseObjFree(SQLiteDatabase db) {
+ return mPool.get(db.mConnectionNum - 1).isFree();
+ }
+
+ /** only used for testing purposes. */
+ /* package */ int getSize() {
+ return mPool.size();
+ }
+
+ /**
+ * represents objects in the connection pool.
+ * package-level access for testing purposes only.
+ */
+ /* package */ static class PoolObj {
+
+ private final SQLiteDatabase mDb;
+ private boolean mFreeBusyFlag = FREE;
+ private static final boolean FREE = true;
+ private static final boolean BUSY = false;
+
+ /** the number of threads holding this connection */
+ // @GuardedBy("this")
+ private int mNumHolders = 0;
+
+ /** contains the threadIds of the threads holding this connection.
+ * used for debugging purposes only.
+ */
+ // @GuardedBy("this")
+ private HashSet<Long> mHolderIds = new HashSet<Long>();
+
+ public PoolObj(SQLiteDatabase db) {
+ mDb = db;
+ }
+
+ private synchronized void acquire() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ assert isFree();
+ long id = Thread.currentThread().getId();
+ assert !mHolderIds.contains(id);
+ mHolderIds.add(id);
+ }
+
+ mNumHolders++;
+ mFreeBusyFlag = BUSY;
+ }
+
+ private synchronized void release() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ long id = Thread.currentThread().getId();
+ assert mHolderIds.size() == mNumHolders;
+ assert mHolderIds.contains(id);
+ mHolderIds.remove(id);
+ }
+
+ mNumHolders--;
+ if (mNumHolders == 0) {
+ mFreeBusyFlag = FREE;
+ }
+ }
+
+ private synchronized boolean isFree() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ verify();
+ }
+ return (mFreeBusyFlag == FREE);
+ }
+
+ private synchronized void verify() {
+ if (mFreeBusyFlag == FREE) {
+ assert mNumHolders == 0;
+ } else {
+ assert mNumHolders > 0;
+ }
+ }
+
+ /**
+ * only for testing purposes
+ */
+ /* package */ synchronized int getNumHolders() {
+ return mNumHolders;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buff = new StringBuilder();
+ buff.append(", conn # ");
+ buff.append(mDb.mConnectionNum);
+ buff.append(", mCountHolders = ");
+ synchronized(this) {
+ buff.append(mNumHolders);
+ buff.append(", freeBusyFlag = ");
+ buff.append(mFreeBusyFlag);
+ for (Long l : mHolderIds) {
+ buff.append(", id = " + l);
+ }
+ }
+ return buff.toString();
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java
index 8ac4c0f..f28c70f 100644
--- a/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java
+++ b/core/java/android/database/sqlite/DatabaseObjectNotClosedException.java
@@ -21,13 +21,11 @@ package android.database.sqlite;
* that is not explicitly closed
* @hide
*/
-public class DatabaseObjectNotClosedException extends RuntimeException
-{
+public class DatabaseObjectNotClosedException extends RuntimeException {
private static final String s = "Application did not close the cursor or database object " +
"that was opened here";
- public DatabaseObjectNotClosedException()
- {
+ public DatabaseObjectNotClosedException() {
super(s);
}
}
diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java
index 25aa9b3..9889a21 100644
--- a/core/java/android/database/sqlite/SQLiteCompiledSql.java
+++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java
@@ -78,20 +78,13 @@ import android.util.Log;
* existing compiled SQL program already around
*/
private void compile(String sql, boolean forceCompilation) {
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
+ mDatabase.verifyLockOwner();
// Only compile if we don't have a valid statement already or the caller has
// explicitly requested a recompile.
if (forceCompilation) {
- mDatabase.lock();
- try {
- // Note that the native_compile() takes care of destroying any previously
- // existing programs before it compiles.
- native_compile(sql);
- } finally {
- mDatabase.unlock();
- }
+ // Note that the native_compile() takes care of destroying any previously
+ // existing programs before it compiles.
+ native_compile(sql);
}
}
@@ -102,13 +95,8 @@ import android.util.Log;
if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
Log.v(TAG, "closed and deallocated DbObj (id#" + nStatement +")");
}
- try {
- mDatabase.lock();
- native_finalize();
- nStatement = 0;
- } finally {
- mDatabase.unlock();
- }
+ mDatabase.finalizeStatementLater(nStatement);
+ nStatement = 0;
}
}
@@ -134,6 +122,10 @@ import android.util.Log;
mInUse = false;
}
+ /* package */ synchronized boolean isInUse() {
+ return mInUse;
+ }
+
/**
* Make sure that the native resource is cleaned up.
*/
@@ -162,5 +154,4 @@ import android.util.Log;
* @param sql The SQL to compile.
*/
private final native void native_compile(String sql);
- private final native void native_finalize();
}
diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java
index c7e58fa..eecd01e 100644
--- a/core/java/android/database/sqlite/SQLiteCursor.java
+++ b/core/java/android/database/sqlite/SQLiteCursor.java
@@ -16,20 +16,19 @@
package android.database.sqlite;
+import android.app.ActivityThread;
import android.database.AbstractWindowedCursor;
import android.database.CursorWindow;
import android.database.DataSetObserver;
-import android.database.SQLException;
-
+import android.database.RequeryOnUiThreadException;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
import android.os.Process;
-import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
@@ -77,6 +76,11 @@ public class SQLiteCursor extends AbstractWindowedCursor {
private int mCursorState = 0;
private ReentrantLock mLock = null;
private boolean mPendingData = false;
+
+ /**
+ * Used by {@link #requery()} to remember for which database we've already shown the warning.
+ */
+ private static final HashMap<String, Boolean> sAlreadyWarned = new HashMap<String, Boolean>();
/**
* support for a cursor variant that doesn't always read all results
@@ -321,166 +325,11 @@ public class SQLiteCursor extends AbstractWindowedCursor {
}
}
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean deleteRow() {
- checkPosition();
-
- // Only allow deletes if there is an ID column, and the ID has been read from it
- if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
- Log.e(TAG,
- "Could not delete row because either the row ID column is not available or it" +
- "has not been read.");
- return false;
- }
-
- boolean success;
-
- /*
- * Ensure we don't change the state of the database when another
- * thread is holding the database lock. requery() and moveTo() are also
- * synchronized here to make sure they get the state of the database
- * immediately following the DELETE.
- */
- mDatabase.lock();
- try {
- try {
- mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
- new String[] {mCurrentRowID.toString()});
- success = true;
- } catch (SQLException e) {
- success = false;
- }
-
- int pos = mPos;
- requery();
-
- /*
- * Ensure proper cursor state. Note that mCurrentRowID changes
- * in this call.
- */
- moveToPosition(pos);
- } finally {
- mDatabase.unlock();
- }
-
- if (success) {
- onChange(true);
- return true;
- } else {
- return false;
- }
- }
-
@Override
public String[] getColumnNames() {
return mColumns;
}
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean supportsUpdates() {
- return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
- }
-
- /**
- * @hide
- * @deprecated
- */
- @Override
- public boolean commitUpdates(Map<? extends Long,
- ? extends Map<String, Object>> additionalValues) {
- if (!supportsUpdates()) {
- Log.e(TAG, "commitUpdates not supported on this cursor, did you "
- + "include the _id column?");
- return false;
- }
-
- /*
- * Prevent other threads from changing the updated rows while they're
- * being processed here.
- */
- synchronized (mUpdatedRows) {
- if (additionalValues != null) {
- mUpdatedRows.putAll(additionalValues);
- }
-
- if (mUpdatedRows.size() == 0) {
- return true;
- }
-
- /*
- * Prevent other threads from changing the database state while
- * we process the updated rows, and prevents us from changing the
- * database behind the back of another thread.
- */
- mDatabase.beginTransaction();
- try {
- StringBuilder sql = new StringBuilder(128);
-
- // For each row that has been updated
- for (Map.Entry<Long, Map<String, Object>> rowEntry :
- mUpdatedRows.entrySet()) {
- Map<String, Object> values = rowEntry.getValue();
- Long rowIdObj = rowEntry.getKey();
-
- if (rowIdObj == null || values == null) {
- throw new IllegalStateException("null rowId or values found! rowId = "
- + rowIdObj + ", values = " + values);
- }
-
- if (values.size() == 0) {
- continue;
- }
-
- long rowId = rowIdObj.longValue();
-
- Iterator<Map.Entry<String, Object>> valuesIter =
- values.entrySet().iterator();
-
- sql.setLength(0);
- sql.append("UPDATE " + mEditTable + " SET ");
-
- // For each column value that has been updated
- Object[] bindings = new Object[values.size()];
- int i = 0;
- while (valuesIter.hasNext()) {
- Map.Entry<String, Object> entry = valuesIter.next();
- sql.append(entry.getKey());
- sql.append("=?");
- bindings[i] = entry.getValue();
- if (valuesIter.hasNext()) {
- sql.append(", ");
- }
- i++;
- }
-
- sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
- + '=' + rowId);
- sql.append(';');
- mDatabase.execSQL(sql.toString(), bindings);
- mDatabase.rowUpdated(mEditTable, rowId);
- }
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
-
- mUpdatedRows.clear();
- }
-
- // Let any change observers know about the update
- onChange(true);
-
- return true;
- }
-
private void deactivateCommon() {
if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
mCursorState = 0;
@@ -506,11 +355,30 @@ public class SQLiteCursor extends AbstractWindowedCursor {
mDriver.cursorClosed();
}
+ /**
+ * Show a warning against the use of requery() if called on the main thread.
+ * This warning is shown per database per process.
+ */
+ private void warnIfUiThread() {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ String databasePath = mDatabase.getPath();
+ // We show the warning once per database in order not to spam logcat.
+ if (!sAlreadyWarned.containsKey(databasePath)) {
+ sAlreadyWarned.put(databasePath, true);
+ String packageName = ActivityThread.currentPackageName();
+ Log.w(TAG, "should not attempt requery on main (UI) thread: app = " +
+ packageName == null ? "'unknown'" : packageName,
+ new RequeryOnUiThreadException(packageName));
+ }
+ }
+ }
+
@Override
public boolean requery() {
if (isClosed()) {
return false;
}
+ warnIfUiThread();
long timeStart = 0;
if (Config.LOGV) {
timeStart = System.currentTimeMillis();
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index cdc9bbb..441370a 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -16,16 +16,16 @@
package android.database.sqlite;
-import com.google.android.collect.Maps;
-
-import android.app.ActivityThread;
import android.app.AppGlobals;
import android.content.ContentValues;
import android.database.Cursor;
+import android.database.DatabaseErrorHandler;
import android.database.DatabaseUtils;
+import android.database.DefaultDatabaseErrorHandler;
import android.database.SQLException;
import android.database.sqlite.SQLiteDebug.DbStats;
import android.os.Debug;
+import android.os.StatFs;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.text.TextUtils;
@@ -38,11 +38,11 @@ import dalvik.system.BlockGuard;
import java.io.File;
import java.lang.ref.WeakReference;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
@@ -233,48 +233,81 @@ public class SQLiteDatabase extends SQLiteClosable {
// lock acquistions of the database.
/* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:";
- /** Used by native code, do not rename */
- /* package */ int mNativeHandle = 0;
+ /** Used by native code, do not rename. make it volatile, so it is thread-safe. */
+ /* package */ volatile int mNativeHandle = 0;
/** Used to make temp table names unique */
/* package */ int mTempTableSequence = 0;
+ /**
+ * The size, in bytes, of a block on "/data". This corresponds to the Unix
+ * statfs.f_bsize field. note that this field is lazily initialized.
+ */
+ private static int sBlockSize = 0;
+
/** The path for the database file */
- private String mPath;
+ private final String mPath;
/** The anonymized path for the database file for logging purposes */
private String mPathForLogs = null; // lazily populated
/** The flags passed to open/create */
- private int mFlags;
+ private final int mFlags;
/** The optional factory to use when creating new Cursors */
- private CursorFactory mFactory;
+ private final CursorFactory mFactory;
- private WeakHashMap<SQLiteClosable, Object> mPrograms;
+ private final WeakHashMap<SQLiteClosable, Object> mPrograms;
/**
- * for each instance of this class, a cache is maintained to store
+ * for each instance of this class, a LRU cache is maintained to store
* the compiled query statement ids returned by sqlite database.
- * key = sql statement with "?" for bind args
+ * key = SQL statement with "?" for bind args
* value = {@link SQLiteCompiledSql}
* If an application opens the database and keeps it open during its entire life, then
- * there will not be an overhead of compilation of sql statements by sqlite.
+ * there will not be an overhead of compilation of SQL statements by sqlite.
*
* why is this cache NOT static? because sqlite attaches compiledsql statements to the
* struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is
* invoked.
*
* this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method
- * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because
- * most of the apps don't use "?" syntax in their sql, caching is not useful for them.
- */
- /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = Maps.newHashMap();
+ * (@link setMaxSqlCacheSize(int)}).
+ */
+ // default statement-cache size per database connection ( = instance of this class)
+ private int mMaxSqlCacheSize = 25;
+ /* package */ final Map<String, SQLiteCompiledSql> mCompiledQueries =
+ new LinkedHashMap<String, SQLiteCompiledSql>(mMaxSqlCacheSize + 1, 0.75f, true) {
+ @Override
+ public boolean removeEldestEntry(Map.Entry<String, SQLiteCompiledSql> eldest) {
+ // eldest = least-recently used entry
+ // if it needs to be removed to accommodate a new entry,
+ // close {@link SQLiteCompiledSql} represented by this entry, if not in use
+ // and then let it be removed from the Map.
+ // when this is called, the caller must be trying to add a just-compiled stmt
+ // to cache; i.e., caller should already have acquired database lock AND
+ // the lock on mCompiledQueries. do as assert of these two 2 facts.
+ verifyLockOwner();
+ if (this.size() <= mMaxSqlCacheSize) {
+ // cache is not full. nothing needs to be removed
+ return false;
+ }
+ // cache is full. eldest will be removed.
+ SQLiteCompiledSql entry = eldest.getValue();
+ if (!entry.isInUse()) {
+ // this {@link SQLiteCompiledSql} is not in use. release it.
+ entry.releaseSqlStatement();
+ }
+ // return true, so that this entry is removed automatically by the caller.
+ return true;
+ }
+ };
/**
- * @hide
+ * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}
+ * size of each prepared-statement is between 1K - 6K, depending on the complexity of the
+ * SQL statement & schema.
*/
- public static final int MAX_SQL_CACHE_SIZE = 250;
- private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance
+ public static final int MAX_SQL_CACHE_SIZE = 100;
private int mCacheFullWarnings;
private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1;
@@ -282,45 +315,49 @@ public class SQLiteDatabase extends SQLiteClosable {
private int mNumCacheHits;
private int mNumCacheMisses;
- /** the following 2 members maintain the time when a database is opened and closed */
- private String mTimeOpened = null;
- private String mTimeClosed = null;
-
/** Used to find out where this object was created in case it never got closed. */
- private Throwable mStackTrace = null;
+ private final Throwable mStackTrace;
// System property that enables logging of slow queries. Specify the threshold in ms.
private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold";
private final int mSlowQueryThreshold;
- /**
- * @param closable
+ /** stores the list of statement ids that need to be finalized by sqlite */
+ private final ArrayList<Integer> mClosedStatementIds = new ArrayList<Integer>();
+
+ /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors
+ * Corruption
+ * */
+ private final DatabaseErrorHandler mErrorHandler;
+
+ /** The Database connection pool {@link DatabaseConnectionPool}.
+ * Visibility is package-private for testing purposes. otherwise, private visibility is enough.
*/
- void addSQLiteClosable(SQLiteClosable closable) {
- lock();
- try {
- mPrograms.put(closable, null);
- } finally {
- unlock();
- }
+ /* package */ volatile DatabaseConnectionPool mConnectionPool = null;
+
+ /** Each database connection handle in the pool is assigned a number 1..N, where N is the
+ * size of the connection pool.
+ * The main connection handle to which the pool is attached is assigned a value of 0.
+ */
+ /* package */ final short mConnectionNum;
+
+ private static final String MEMORY_DB_PATH = ":memory:";
+
+ synchronized void addSQLiteClosable(SQLiteClosable closable) {
+ // mPrograms is per instance of SQLiteDatabase and it doesn't actually touch the database
+ // itself. so, there is no need to lock().
+ mPrograms.put(closable, null);
}
- void removeSQLiteClosable(SQLiteClosable closable) {
- lock();
- try {
- mPrograms.remove(closable);
- } finally {
- unlock();
- }
+ synchronized void removeSQLiteClosable(SQLiteClosable closable) {
+ mPrograms.remove(closable);
}
@Override
protected void onAllReferencesReleased() {
if (isOpen()) {
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- mTimeClosed = getTime();
- }
- dbclose();
+ // close the database which will close all pending statements to be finalized also
+ close();
}
}
@@ -350,19 +387,8 @@ public class SQLiteDatabase extends SQLiteClosable {
private boolean mLockingEnabled = true;
/* package */ void onCorruption() {
- Log.e(TAG, "Removing corrupt database: " + mPath);
EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
- try {
- // Close the database (if we can), which will cause subsequent operations to fail.
- close();
- } finally {
- // Delete the corrupt file. Don't re-create it now -- that would just confuse people
- // -- but the next time someone tries to open it, they can set it up from scratch.
- if (!mPath.equalsIgnoreCase(":memory")) {
- // delete is only for non-memory database files
- new File(mPath).delete();
- }
- }
+ mErrorHandler.onCorruption(this);
}
/**
@@ -460,11 +486,14 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
+ * Begins a transaction in EXCLUSIVE mode.
+ * <p>
+ * Transactions can be nested.
+ * When the outer transaction is ended all of
* the work done in that transaction and all of the nested transactions will be committed or
* rolled back. The changes will be rolled back if any transaction is ended without being
* marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
- *
+ * </p>
* <p>Here is the standard idiom for transactions:
*
* <pre>
@@ -478,15 +507,42 @@ public class SQLiteDatabase extends SQLiteClosable {
* </pre>
*/
public void beginTransaction() {
- beginTransactionWithListener(null /* transactionStatusCallback */);
+ beginTransaction(null /* transactionStatusCallback */, true);
}
/**
- * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
+ * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When
+ * the outer transaction is ended all of the work done in that transaction
+ * and all of the nested transactions will be committed or rolled back. The
+ * changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they
+ * will be committed.
+ * <p>
+ * Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransactionNonExclusive();
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ */
+ public void beginTransactionNonExclusive() {
+ beginTransaction(null /* transactionStatusCallback */, false);
+ }
+
+ /**
+ * Begins a transaction in EXCLUSIVE mode.
+ * <p>
+ * Transactions can be nested.
+ * When the outer transaction is ended all of
* the work done in that transaction and all of the nested transactions will be committed or
* rolled back. The changes will be rolled back if any transaction is ended without being
* marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
- *
+ * </p>
* <p>Here is the standard idiom for transactions:
*
* <pre>
@@ -498,15 +554,48 @@ public class SQLiteDatabase extends SQLiteClosable {
* db.endTransaction();
* }
* </pre>
+ *
* @param transactionListener listener that should be notified when the transaction begins,
* commits, or is rolled back, either explicitly or by a call to
* {@link #yieldIfContendedSafely}.
*/
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
+ beginTransaction(transactionListener, true);
+ }
+
+ /**
+ * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When
+ * the outer transaction is ended all of the work done in that transaction
+ * and all of the nested transactions will be committed or rolled back. The
+ * changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they
+ * will be committed.
+ * <p>
+ * Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransactionWithListenerNonExclusive(listener);
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ *
+ * @param transactionListener listener that should be notified when the
+ * transaction begins, commits, or is rolled back, either
+ * explicitly or by a call to {@link #yieldIfContendedSafely}.
+ */
+ public void beginTransactionWithListenerNonExclusive(
+ SQLiteTransactionListener transactionListener) {
+ beginTransaction(transactionListener, false);
+ }
+
+ private void beginTransaction(SQLiteTransactionListener transactionListener,
+ boolean exclusive) {
+ verifyDbIsOpen();
lockForced();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
boolean ok = false;
try {
// If this thread already had the lock then get out
@@ -524,7 +613,14 @@ public class SQLiteDatabase extends SQLiteClosable {
// This thread didn't already have the lock, so begin a database
// transaction now.
- execSQL("BEGIN EXCLUSIVE;");
+ // STOPSHIP - uncomment the following 1 line
+ // if (exclusive) {
+ // STOPSHIP - remove the following 1 line
+ if (exclusive && mConnectionPool == null) {
+ execSQL("BEGIN EXCLUSIVE;");
+ } else {
+ execSQL("BEGIN IMMEDIATE;");
+ }
mTransactionListener = transactionListener;
mTransactionIsSuccessful = true;
mInnerTransactionIsSuccessful = false;
@@ -551,12 +647,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* are committed and rolled back.
*/
public void endTransaction() {
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- if (!mLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("no transaction pending");
- }
+ verifyLockOwner();
try {
if (mInnerTransactionIsSuccessful) {
mInnerTransactionIsSuccessful = false;
@@ -581,6 +672,18 @@ public class SQLiteDatabase extends SQLiteClosable {
}
if (mTransactionIsSuccessful) {
execSQL(COMMIT_SQL);
+ // if write-ahead logging is used, we have to take care of checkpoint.
+ // TODO: should applications be given the flexibility of choosing when to
+ // trigger checkpoint?
+ // for now, do checkpoint after every COMMIT because that is the fastest
+ // way to guarantee that readers will see latest data.
+ // but this is the slowest way to run sqlite with in write-ahead logging mode.
+ if (this.mConnectionPool != null) {
+ execSQL("PRAGMA wal_checkpoint;");
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ Log.i(TAG, "PRAGMA wal_Checkpoint done");
+ }
+ }
} else {
try {
execSQL("ROLLBACK;");
@@ -614,9 +717,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* transaction is already marked as successful.
*/
public void setTransactionSuccessful() {
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
+ verifyDbIsOpen();
if (!mLock.isHeldByCurrentThread()) {
throw new IllegalStateException("no transaction pending");
}
@@ -814,30 +915,72 @@ public class SQLiteDatabase extends SQLiteClosable {
* @throws SQLiteException if the database cannot be opened
*/
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) {
- SQLiteDatabase sqliteDatabase = null;
+ return openDatabase(path, factory, flags, new DefaultDatabaseErrorHandler());
+ }
+
+ /**
+ * Open the database according to the flags {@link #OPEN_READWRITE}
+ * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
+ *
+ * <p>Sets the locale of the database to the the system's current locale.
+ * Call {@link #setLocale} if you would like something else.</p>
+ *
+ * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
+ * used to handle corruption when sqlite reports database corruption.</p>
+ *
+ * @param path to database file to open and/or create
+ * @param factory an optional factory class that is called to instantiate a
+ * cursor when query is called, or null for default
+ * @param flags to control database access mode
+ * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption
+ * when sqlite reports database corruption
+ * @return the newly opened database
+ * @throws SQLiteException if the database cannot be opened
+ */
+ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
+ DatabaseErrorHandler errorHandler) {
+ SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler,
+ (short) 0 /* the main connection handle */);
+
+ // set sqlite pagesize to mBlockSize
+ if (sBlockSize == 0) {
+ // TODO: "/data" should be a static final String constant somewhere. it is hardcoded
+ // in several places right now.
+ sBlockSize = new StatFs("/data").getBlockSize();
+ }
+ sqliteDatabase.setPageSize(sBlockSize);
+ //STOPSHIP - uncomment the following line
+ //sqliteDatabase.setJournalMode(path, "TRUNCATE");
+ // STOPSHIP remove the following lines
+ sqliteDatabase.enableWriteAheadLogging();
+
+ // add this database to the list of databases opened in this process
+ ActiveDatabases.addActiveDatabase(sqliteDatabase);
+ return sqliteDatabase;
+ }
+
+ private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
+ DatabaseErrorHandler errorHandler, short connectionNum) {
+ SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum);
try {
// Open the database.
- sqliteDatabase = new SQLiteDatabase(path, factory, flags);
+ db.dbopen(path, flags);
+ db.setLocale(Locale.getDefault());
if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
- sqliteDatabase.enableSqlTracing(path);
+ db.enableSqlTracing(path, connectionNum);
}
if (SQLiteDebug.DEBUG_SQL_TIME) {
- sqliteDatabase.enableSqlProfiling(path);
+ db.enableSqlProfiling(path, connectionNum);
}
+ return db;
} catch (SQLiteDatabaseCorruptException e) {
- // Try to recover from this, if we can.
- // TODO: should we do this for other open failures?
- Log.e(TAG, "Deleting and re-creating corrupt database " + path, e);
- EventLog.writeEvent(EVENT_DB_CORRUPT, path);
- if (!path.equalsIgnoreCase(":memory")) {
- // delete is only for non-memory database files
- new File(path).delete();
- }
- sqliteDatabase = new SQLiteDatabase(path, factory, flags);
+ db.mErrorHandler.onCorruption(db);
+ return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler);
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Failed to open the database. closing it.", e);
+ db.close();
+ throw e;
}
- ActiveDatabases.getInstance().mActiveDatabases.add(
- new WeakReference<SQLiteDatabase>(sqliteDatabase));
- return sqliteDatabase;
}
/**
@@ -855,6 +998,25 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
+ * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler).
+ */
+ public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory,
+ DatabaseErrorHandler errorHandler) {
+ return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler);
+ }
+
+ private void setJournalMode(final String dbPath, final String mode) {
+ // journal mode can be set only for non-memory databases
+ if (!dbPath.equalsIgnoreCase(MEMORY_DB_PATH)) {
+ String s = DatabaseUtils.stringForQuery(this, "PRAGMA journal_mode=" + mode, null);
+ if (!s.equalsIgnoreCase(mode)) {
+ Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + dbPath +
+ " (on pragma set journal_mode, sqlite returned:" + s);
+ }
+ }
+ }
+
+ /**
* Create a memory backed SQLite database. Its contents will be destroyed
* when the database is closed.
*
@@ -867,7 +1029,7 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public static SQLiteDatabase create(CursorFactory factory) {
// This is a magic string with special meaning for SQLite.
- return openDatabase(":memory:", factory, CREATE_IF_NECESSARY);
+ return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY);
}
/**
@@ -877,18 +1039,26 @@ public class SQLiteDatabase extends SQLiteClosable {
if (!isOpen()) {
return; // already closed
}
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum);
+ }
lock();
try {
closeClosable();
+ // finalize ALL statements queued up so far
+ closePendingStatements();
// close this database instance - regardless of its reference count value
- onAllReferencesReleased();
+ dbclose();
+ if (mConnectionPool != null) {
+ mConnectionPool.close();
+ }
} finally {
unlock();
}
}
private void closeClosable() {
- /* deallocate all compiled sql statement objects from mCompiledQueries cache.
+ /* deallocate all compiled SQL statement objects from mCompiledQueries cache.
* this should be done before de-referencing all {@link SQLiteClosable} objects
* from this database object because calling
* {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database
@@ -918,19 +1088,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return the database version
*/
public int getVersion() {
- SQLiteStatement prog = null;
- lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- try {
- prog = new SQLiteStatement(this, "PRAGMA user_version;");
- long version = prog.simpleQueryForLong();
- return (int) version;
- } finally {
- if (prog != null) prog.close();
- unlock();
- }
+ return ((Long) DatabaseUtils.longForQuery(this, "PRAGMA user_version;", null)).intValue();
}
/**
@@ -948,20 +1106,8 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return the new maximum database size
*/
public long getMaximumSize() {
- SQLiteStatement prog = null;
- lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- try {
- prog = new SQLiteStatement(this,
- "PRAGMA max_page_count;");
- long pageCount = prog.simpleQueryForLong();
- return pageCount * getPageSize();
- } finally {
- if (prog != null) prog.close();
- unlock();
- }
+ long pageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count;", null);
+ return pageCount * getPageSize();
}
/**
@@ -972,26 +1118,15 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return the new maximum database size
*/
public long setMaximumSize(long numBytes) {
- SQLiteStatement prog = null;
- lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- try {
- long pageSize = getPageSize();
- long numPages = numBytes / pageSize;
- // If numBytes isn't a multiple of pageSize, bump up a page
- if ((numBytes % pageSize) != 0) {
- numPages++;
- }
- prog = new SQLiteStatement(this,
- "PRAGMA max_page_count = " + numPages);
- long newPageCount = prog.simpleQueryForLong();
- return newPageCount * pageSize;
- } finally {
- if (prog != null) prog.close();
- unlock();
+ long pageSize = getPageSize();
+ long numPages = numBytes / pageSize;
+ // If numBytes isn't a multiple of pageSize, bump up a page
+ if ((numBytes % pageSize) != 0) {
+ numPages++;
}
+ long newPageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count = " + numPages,
+ null);
+ return newPageCount * pageSize;
}
/**
@@ -1000,20 +1135,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return the database page size, in bytes
*/
public long getPageSize() {
- SQLiteStatement prog = null;
- lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- try {
- prog = new SQLiteStatement(this,
- "PRAGMA page_size;");
- long size = prog.simpleQueryForLong();
- return size;
- } finally {
- if (prog != null) prog.close();
- unlock();
- }
+ return DatabaseUtils.longForQuery(this, "PRAGMA page_size;", null);
}
/**
@@ -1101,7 +1223,7 @@ public class SQLiteDatabase extends SQLiteClosable {
if (info != null) {
execSQL("UPDATE " + info.masterTable
+ " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey
- + " FROM " + table + " WHERE _id=" + rowId + ")");
+ + " FROM " + table + " WHERE _id=?)", new String[] {String.valueOf(rowId)});
}
}
@@ -1134,6 +1256,8 @@ public class SQLiteDatabase extends SQLiteClosable {
* statement and fill in those values with {@link SQLiteProgram#bindString}
* and {@link SQLiteProgram#bindLong} each time you want to run the
* statement. Statements may not return result sets larger than 1x1.
+ *<p>
+ * No two threads should be using the same {@link SQLiteStatement} at the same time.
*
* @param sql The raw SQL statement, may contain ? for unknown values to be
* bound later.
@@ -1141,15 +1265,8 @@ public class SQLiteDatabase extends SQLiteClosable {
* {@link SQLiteStatement}s are not synchronized, see the documentation for more details.
*/
public SQLiteStatement compileStatement(String sql) throws SQLException {
- lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- try {
- return new SQLiteStatement(this, sql);
- } finally {
- unlock();
- }
+ verifyDbIsOpen();
+ return new SQLiteStatement(this, sql);
}
/**
@@ -1226,9 +1343,7 @@ public class SQLiteDatabase extends SQLiteClosable {
boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit) {
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
+ verifyDbIsOpen();
String sql = SQLiteQueryBuilder.buildQueryString(
distinct, table, columns, selection, groupBy, having, orderBy, limit);
@@ -1339,9 +1454,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public Cursor rawQueryWithFactory(
CursorFactory cursorFactory, String sql, String[] selectionArgs,
String editTable) {
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
+ verifyDbIsOpen();
BlockGuard.getThreadPolicy().onReadFromDisk();
long timeStart = 0;
@@ -1349,7 +1462,8 @@ public class SQLiteDatabase extends SQLiteClosable {
timeStart = System.currentTimeMillis();
}
- SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable);
+ SQLiteDatabase db = getDbConnection(sql);
+ SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable);
Cursor cursor = null;
try {
@@ -1375,6 +1489,7 @@ public class SQLiteDatabase extends SQLiteClosable {
: "<null>") + ", count is " + count);
}
}
+ releaseDbConnection(db);
}
return cursor;
}
@@ -1501,10 +1616,8 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public long insertWithOnConflict(String table, String nullColumnHack,
ContentValues initialValues, int conflictAlgorithm) {
+ verifyDbIsOpen();
BlockGuard.getThreadPolicy().onWriteToDisk();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
// Measurements show most sql lengths <= 152
StringBuilder sql = new StringBuilder(152);
@@ -1593,11 +1706,9 @@ public class SQLiteDatabase extends SQLiteClosable {
* whereClause.
*/
public int delete(String table, String whereClause, String[] whereArgs) {
+ verifyDbIsOpen();
BlockGuard.getThreadPolicy().onWriteToDisk();
lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
SQLiteStatement statement = null;
try {
statement = compileStatement("DELETE FROM " + table
@@ -1677,10 +1788,8 @@ public class SQLiteDatabase extends SQLiteClosable {
sql.append(whereClause);
}
+ verifyDbIsOpen();
lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
SQLiteStatement statement = null;
try {
statement = compileStatement(sql.toString());
@@ -1725,21 +1834,39 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Execute a single SQL statement that is not a query. For example, CREATE
- * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
- * supported. it takes a write lock
+ * Execute a single SQL statement that is NOT a SELECT
+ * or any other SQL statement that returns data.
+ * <p>
+ * Use of this method is discouraged as it doesn't perform well when issuing the same SQL
+ * statement repeatedly (see {@link #compileStatement(String)} to prepare statements for
+ * repeated use), and it has no means to return any data (such as the number of affected rows).
+ * Instead, you're encouraged to use {@link #insert(String, String, ContentValues)},
+ * {@link #update(String, ContentValues, String, String[])}, et al, when possible.
+ * </p>
+ * <p>
+ * When using {@link #enableWriteAheadLogging()}, journal_mode is
+ * automatically managed by this class. So, do not set journal_mode
+ * using "PRAGMA journal_mode'<value>" statement if your app is using
+ * {@link #enableWriteAheadLogging()}
+ * </p>
*
+ * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
+ * not supported.
* @throws SQLException If the SQL string is invalid for some reason
*/
public void execSQL(String sql) throws SQLException {
+ sql = sql.trim();
+ String prefix = sql.substring(0, 6);
+ if (prefix.equalsIgnoreCase("ATTACH")) {
+ disableWriteAheadLogging();
+ }
+ verifyDbIsOpen();
BlockGuard.getThreadPolicy().onWriteToDisk();
long timeStart = SystemClock.uptimeMillis();
lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX);
try {
+ closePendingStatements();
native_execSQL(sql);
} catch (SQLiteDatabaseCorruptException e) {
onCorruption();
@@ -1759,11 +1886,45 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Execute a single SQL statement that is not a query. For example, CREATE
- * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
- * supported. it takes a write lock,
+ * Execute a single SQL statement that is NOT a SELECT/INSERT/UPDATE/DELETE.
+ * <p>
+ * For INSERT statements, use any of the following instead.
+ * <ul>
+ * <li>{@link #insert(String, String, ContentValues)}</li>
+ * <li>{@link #insertOrThrow(String, String, ContentValues)}</li>
+ * <li>{@link #insertWithOnConflict(String, String, ContentValues, int)}</li>
+ * </ul>
+ * <p>
+ * For UPDATE statements, use any of the following instead.
+ * <ul>
+ * <li>{@link #update(String, ContentValues, String, String[])}</li>
+ * <li>{@link #updateWithOnConflict(String, ContentValues, String, String[], int)}</li>
+ * </ul>
+ * <p>
+ * For DELETE statements, use any of the following instead.
+ * <ul>
+ * <li>{@link #delete(String, String, String[])}</li>
+ * </ul>
+ * <p>
+ * For example, the following are good candidates for using this method:
+ * <ul>
+ * <li>ALTER TABLE</li>
+ * <li>CREATE or DROP table / trigger / view / index / virtual table</li>
+ * <li>REINDEX</li>
+ * <li>RELEASE</li>
+ * <li>SAVEPOINT</li>
+ * <li>PRAGMA that returns no data</li>
+ * </ul>
+ * </p>
+ * <p>
+ * When using {@link #enableWriteAheadLogging()}, journal_mode is
+ * automatically managed by this class. So, do not set journal_mode
+ * using "PRAGMA journal_mode'<value>" statement if your app is using
+ * {@link #enableWriteAheadLogging()}
+ * </p>
*
- * @param sql
+ * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
+ * not supported.
* @param bindArgs only byte[], String, Long and Double are supported in bindArgs.
* @throws SQLException If the SQL string is invalid for some reason
*/
@@ -1772,11 +1933,9 @@ public class SQLiteDatabase extends SQLiteClosable {
if (bindArgs == null) {
throw new IllegalArgumentException("Empty bindArgs");
}
+ verifyDbIsOpen();
long timeStart = SystemClock.uptimeMillis();
lock();
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
SQLiteStatement statement = null;
try {
statement = compileStatement(sql);
@@ -1810,14 +1969,19 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Private constructor. See {@link #create} and {@link #openDatabase}.
+ * Private constructor.
*
* @param path The full path to the database
* @param factory The factory to use when creating cursors, may be NULL.
* @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already
* exists, mFlags will be updated appropriately.
+ * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite reports database
+ * corruption. may be NULL.
+ * @param connectionNum 0 for main database connection handle. 1..N for pooled database
+ * connection handles.
*/
- private SQLiteDatabase(String path, CursorFactory factory, int flags) {
+ private SQLiteDatabase(String path, CursorFactory factory, int flags,
+ DatabaseErrorHandler errorHandler, short connectionNum) {
if (path == null) {
throw new IllegalArgumentException("path should not be null");
}
@@ -1826,25 +1990,11 @@ public class SQLiteDatabase extends SQLiteClosable {
mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1);
mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
mFactory = factory;
- dbopen(mPath, mFlags);
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- mTimeOpened = getTime();
- }
mPrograms = new WeakHashMap<SQLiteClosable,Object>();
- try {
- setLocale(Locale.getDefault());
- } catch (RuntimeException e) {
- Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e);
- dbclose();
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- mTimeClosed = getTime();
- }
- throw e;
- }
- }
-
- private String getTime() {
- return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis());
+ // Set the DatabaseErrorHandler to be used when SQLite reports corruption.
+ // If the caller sets errorHandler = null, then use default errorhandler.
+ mErrorHandler = (errorHandler == null) ? new DefaultDatabaseErrorHandler() : errorHandler;
+ mConnectionNum = connectionNum;
}
/**
@@ -1970,6 +2120,20 @@ public class SQLiteDatabase extends SQLiteClosable {
}
}
+ /* package */ void verifyDbIsOpen() {
+ if (!isOpen()) {
+ throw new IllegalStateException("database " + getPath() + " (conn# " +
+ mConnectionNum + ") already closed");
+ }
+ }
+
+ /* package */ void verifyLockOwner() {
+ verifyDbIsOpen();
+ if (mLockingEnabled && !isDbLockedByCurrentThread()) {
+ throw new IllegalStateException("Don't have database lock!");
+ }
+ }
+
/*
* ============================================================================
*
@@ -1977,22 +2141,14 @@ public class SQLiteDatabase extends SQLiteClosable {
* ============================================================================
*/
/**
- * adds the given sql and its compiled-statement-id-returned-by-sqlite to the
+ * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the
* cache of compiledQueries attached to 'this'.
- *
- * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql,
+ * <p>
+ * If there is already a {@link SQLiteCompiledSql} in compiledQueries for the given SQL,
* the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current
* mapping is NOT replaced with the new mapping).
*/
/* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) {
- if (mMaxSqlCacheSize == 0) {
- // for this database, there is no cache of compiled sql.
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql);
- }
- return;
- }
-
SQLiteCompiledSql compiledSql = null;
synchronized(mCompiledQueries) {
// don't insert the new mapping if a mapping already exists
@@ -2000,35 +2156,30 @@ public class SQLiteDatabase extends SQLiteClosable {
if (compiledSql != null) {
return;
}
- // add this <sql, compiledStatement> to the cache
+
if (mCompiledQueries.size() == mMaxSqlCacheSize) {
/*
* cache size of {@link #mMaxSqlCacheSize} is not enough for this app.
- * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times
- * chances are it is NOT using ? for bindargs - so caching is useless.
- * TODO: either let the callers set max cchesize for their app, or intelligently
- * figure out what should be cached for a given app.
+ * log a warning.
+ * chances are it is NOT using ? for bindargs - or cachesize is too small.
*/
if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) {
Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " +
- getPath() + "; i.e., NO space for this sql statement in cache: " +
- sql + ". Please change your sql statements to use '?' for " +
- "bindargs, instead of using actual values");
- }
- // don't add this entry to cache
- } else {
- // cache is NOT full. add this to cache.
- mCompiledQueries.put(sql, compiledStatement);
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" +
- mCompiledQueries.size() + "|" + sql);
+ getPath() + ". Consider increasing cachesize.");
}
+ }
+ /* add the given SQLiteCompiledSql compiledStatement to cache.
+ * no need to worry about the cache size - because {@link #mCompiledQueries}
+ * self-limits its size to {@link #mMaxSqlCacheSize}.
+ */
+ mCompiledQueries.put(sql, compiledStatement);
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" +
+ mCompiledQueries.size() + "|" + sql);
}
}
- return;
}
-
private void deallocCachedSqlStatements() {
synchronized (mCompiledQueries) {
for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) {
@@ -2039,20 +2190,13 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * from the compiledQueries cache, returns the compiled-statement-id for the given sql.
- * returns null, if not found in the cache.
+ * From the compiledQueries cache, returns the compiled-statement-id for the given SQL.
+ * Returns null, if not found in the cache.
*/
/* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) {
SQLiteCompiledSql compiledStatement = null;
boolean cacheHit;
synchronized(mCompiledQueries) {
- if (mMaxSqlCacheSize == 0) {
- // for this database, there is no cache of compiled sql.
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- Log.v(TAG, "|cache NOT found|" + getPath());
- }
- return null;
- }
cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null;
}
if (cacheHit) {
@@ -2065,80 +2209,248 @@ public class SQLiteDatabase extends SQLiteClosable {
Log.v(TAG, "|cache_stats|" +
getPath() + "|" + mCompiledQueries.size() +
"|" + mNumCacheHits + "|" + mNumCacheMisses +
- "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql);
+ "|" + cacheHit + "|" + sql);
}
return compiledStatement;
}
/**
- * returns true if the given sql is cached in compiled-sql cache.
- * @hide
+ * Sets the maximum size of the prepared-statement cache for this database.
+ * (size of the cache = number of compiled-sql-statements stored in the cache).
+ *<p>
+ * Maximum cache size can ONLY be increased from its current size (default = 10).
+ * If this method is called with smaller size than the current maximum value,
+ * then IllegalStateException is thrown.
+ *<p>
+ * This method is thread-safe.
+ *
+ * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE})
+ * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or
+ * > the value set with previous setMaxSqlCacheSize() call.
*/
- public boolean isInCompiledSqlCache(String sql) {
- synchronized(mCompiledQueries) {
+ public synchronized void setMaxSqlCacheSize(int cacheSize) {
+ if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
+ throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE);
+ } else if (cacheSize < mMaxSqlCacheSize) {
+ throw new IllegalStateException("cannot set cacheSize to a value less than the value " +
+ "set with previous setMaxSqlCacheSize() call.");
+ }
+ mMaxSqlCacheSize = cacheSize;
+ }
+
+ /* package */ boolean isSqlInStatementCache(String sql) {
+ synchronized (mCompiledQueries) {
return mCompiledQueries.containsKey(sql);
}
}
+ /* package */ void finalizeStatementLater(int id) {
+ if (!isOpen()) {
+ // database already closed. this statement will already have been finalized.
+ return;
+ }
+ synchronized(mClosedStatementIds) {
+ if (mClosedStatementIds.contains(id)) {
+ // this statement id is already queued up for finalization.
+ return;
+ }
+ mClosedStatementIds.add(id);
+ }
+ }
+
/**
- * purges the given sql from the compiled-sql cache.
+ * public visibility only for testing. otherwise, package visibility is sufficient
* @hide
*/
- public void purgeFromCompiledSqlCache(String sql) {
- synchronized(mCompiledQueries) {
- mCompiledQueries.remove(sql);
+ public void closePendingStatements() {
+ if (!isOpen()) {
+ // since this database is already closed, no need to finalize anything.
+ mClosedStatementIds.clear();
+ return;
+ }
+ verifyLockOwner();
+ /* to minimize synchronization on mClosedStatementIds, make a copy of the list */
+ ArrayList<Integer> list = new ArrayList<Integer>(mClosedStatementIds.size());
+ synchronized(mClosedStatementIds) {
+ list.addAll(mClosedStatementIds);
+ mClosedStatementIds.clear();
+ }
+ // finalize all the statements from the copied list
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ native_finalize(list.get(i));
}
}
/**
- * remove everything from the compiled sql cache
+ * for testing only
* @hide
*/
- public void resetCompiledSqlCache() {
- synchronized(mCompiledQueries) {
- mCompiledQueries.clear();
+ public ArrayList<Integer> getQueuedUpStmtList() {
+ return mClosedStatementIds;
+ }
+
+ /**
+ * This method enables parallel execution of queries from multiple threads on the same database.
+ * It does this by opening multiple handles to the database and using a different
+ * database handle for each query.
+ * <p>
+ * If a transaction is in progress on one connection handle and say, a table is updated in the
+ * transaction, then query on the same table on another connection handle will block for the
+ * transaction to complete. But this method enables such queries to execute by having them
+ * return old version of the data from the table. Most often it is the data that existed in the
+ * table prior to the above transaction updates on that table.
+ * <p>
+ * Maximum number of simultaneous handles used to execute queries in parallel is
+ * dependent upon the device memory and possibly other properties.
+ * <p>
+ * After calling this method, execution of queries in parallel is enabled as long as this
+ * database handle is open. To disable execution of queries in parallel, database should
+ * be closed and reopened.
+ * <p>
+ * If a query is part of a transaction, then it is executed on the same database handle the
+ * transaction was begun.
+ * <p>
+ * If the database has any attached databases, then execution of queries in paralel is NOT
+ * possible. In such cases, a message is printed to logcat and false is returned.
+ * <p>
+ * This feature is not available for :memory: databases. In such cases,
+ * a message is printed to logcat and false is returned.
+ * <p>
+ * A typical way to use this method is the following:
+ * <pre>
+ * SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+ * CREATE_IF_NECESSARY, myDatabaseErrorHandler);
+ * db.enableWriteAheadLogging();
+ * </pre>
+ * <p>
+ * Writers should use {@link #beginTransactionNonExclusive()} or
+ * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)}
+ * to start a trsnsaction.
+ * Non-exclusive mode allows database file to be in readable by threads executing queries.
+ * </p>
+ *
+ * @return true if write-ahead-logging is set. false otherwise
+ */
+ public synchronized boolean enableWriteAheadLogging() {
+ if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) {
+ Log.i(TAG, "can't enable WAL for memory databases.");
+ return false;
}
+
+ // make sure this database has NO attached databases because sqlite's write-ahead-logging
+ // doesn't work for databases with attached databases
+ if (getAttachedDbs().size() > 1) {
+ Log.i(TAG, "this database: " + mPath + " has attached databases. can't enable WAL.");
+ return false;
+ }
+ if (mConnectionPool == null) {
+ mConnectionPool = new DatabaseConnectionPool(this);
+ setJournalMode(mPath, "WAL");
+ }
+ return true;
}
/**
- * return the current maxCacheSqlCacheSize
- * @hide
+ * package visibility only for testing purposes
*/
- public synchronized int getMaxSqlCacheSize() {
- return mMaxSqlCacheSize;
+ /* package */ synchronized void disableWriteAheadLogging() {
+ if (mConnectionPool == null) {
+ return;
+ }
+ mConnectionPool.close();
+ mConnectionPool = null;
}
/**
- * set the max size of the compiled sql cache for this database after purging the cache.
- * (size of the cache = number of compiled-sql-statements stored in the cache).
- *
- * max cache size can ONLY be increased from its current size (default = 0).
- * if this method is called with smaller size than the current value of mMaxSqlCacheSize,
- * then IllegalStateException is thrown
+ * Sets the database connection handle pool size to the given value.
+ * Database connection handle pool is enabled when the app calls
+ * {@link #enableWriteAheadLogging()}.
+ * <p>
+ * The default connection handle pool is set by the system by taking into account various
+ * aspects of the device, such as memory, number of cores etc. It is recommended that
+ * applications use the default pool size set by the system.
*
- * synchronized because we don't want t threads to change cache size at the same time.
- * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE)
- * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or
- * < the value set with previous setMaxSqlCacheSize() call.
- *
- * @hide
+ * @param size the value the connection handle pool size should be set to.
*/
- public synchronized void setMaxSqlCacheSize(int cacheSize) {
- if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
- throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE);
- } else if (cacheSize < mMaxSqlCacheSize) {
- throw new IllegalStateException("cannot set cacheSize to a value less than the value " +
- "set with previous setMaxSqlCacheSize() call.");
+ public synchronized void setConnectionPoolSize(int size) {
+ if (mConnectionPool == null) {
+ throw new IllegalStateException("connection pool not enabled");
}
- mMaxSqlCacheSize = cacheSize;
+ int i = mConnectionPool.getMaxPoolSize();
+ if (size < i) {
+ throw new IllegalArgumentException(
+ "cannot set max pool size to a value less than the current max value(=" +
+ i + ")");
+ }
+ mConnectionPool.setMaxPoolSize(size);
+ }
+
+ /* package */ SQLiteDatabase createPoolConnection(short connectionNum) {
+ return openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum);
+ }
+
+ private boolean isPooledConnection() {
+ return this.mConnectionNum > 0;
+ }
+
+ /* package */ SQLiteDatabase getDbConnection(String sql) {
+ verifyDbIsOpen();
+
+ // use the current connection handle if
+ // 1. this is a pooled connection handle
+ // 2. OR, if this thread is in a transaction
+ // 3. OR, if there is NO connection handle pool setup
+ SQLiteDatabase db = null;
+ if (isPooledConnection() ||
+ (inTransaction() && mLock.isHeldByCurrentThread()) ||
+ (this.mConnectionPool == null)) {
+ db = this;
+ } else {
+ // get a connection handle from the pool
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ assert mConnectionPool != null;
+ }
+ db = mConnectionPool.get(sql);
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "getDbConnection threadid = " + Thread.currentThread().getId() +
+ ", request on # " + mConnectionNum +
+ ", assigned # " + db.mConnectionNum + ", " + getPath());
+ }
+ return db;
+ }
+
+ private void releaseDbConnection(SQLiteDatabase db) {
+ // ignore this release call if
+ // 1. the database is closed
+ // 2. OR, if db is NOT a pooled connection handle
+ // 3. OR, if the database being released is same as 'this' (this condition means
+ // that we should always be releasing a pooled connection handle by calling this method
+ // from the 'main' connection handle
+ if (!isOpen() || !db.isPooledConnection() || (db == this)) {
+ return;
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ assert isPooledConnection();
+ assert mConnectionPool != null;
+ Log.d(TAG, "releaseDbConnection threadid = " + Thread.currentThread().getId() +
+ ", releasing # " + db.mConnectionNum + ", " + getPath());
+ }
+ mConnectionPool.release(db);
}
static class ActiveDatabases {
private static final ActiveDatabases activeDatabases = new ActiveDatabases();
private HashSet<WeakReference<SQLiteDatabase>> mActiveDatabases =
- new HashSet<WeakReference<SQLiteDatabase>>();
+ new HashSet<WeakReference<SQLiteDatabase>>();
private ActiveDatabases() {} // disable instantiation of this class
- static ActiveDatabases getInstance() {return activeDatabases;}
+ static ActiveDatabases getInstance() {
+ return activeDatabases;
+ }
+ private static void addActiveDatabase(SQLiteDatabase sqliteDatabase) {
+ activeDatabases.mActiveDatabases.add(new WeakReference<SQLiteDatabase>(sqliteDatabase));
+ }
}
/**
@@ -2152,83 +2464,131 @@ public class SQLiteDatabase extends SQLiteClosable {
if (db == null || !db.isOpen()) {
continue;
}
- // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db
- int lookasideUsed = db.native_getDbLookaside();
- // get the lastnode of the dbname
- String path = db.getPath();
- int indx = path.lastIndexOf("/");
- String lastnode = path.substring((indx != -1) ? ++indx : 0);
+ try {
+ // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db
+ int lookasideUsed = db.native_getDbLookaside();
- // get list of attached dbs and for each db, get its size and pagesize
- ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db);
- if (attachedDbs == null) {
- continue;
- }
- for (int i = 0; i < attachedDbs.size(); i++) {
- Pair<String, String> p = attachedDbs.get(i);
- long pageCount = getPragmaVal(db, p.first + ".page_count;");
-
- // first entry in the attached db list is always the main database
- // don't worry about prefixing the dbname with "main"
- String dbName;
- if (i == 0) {
- dbName = lastnode;
- } else {
- // lookaside is only relevant for the main db
- lookasideUsed = 0;
- dbName = " (attached) " + p.first;
- // if the attached db has a path, attach the lastnode from the path to above
- if (p.second.trim().length() > 0) {
- int idx = p.second.lastIndexOf("/");
- dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0);
+ // get the lastnode of the dbname
+ String path = db.getPath();
+ int indx = path.lastIndexOf("/");
+ String lastnode = path.substring((indx != -1) ? ++indx : 0);
+
+ // get list of attached dbs and for each db, get its size and pagesize
+ ArrayList<Pair<String, String>> attachedDbs = db.getAttachedDbs();
+ if (attachedDbs == null) {
+ continue;
+ }
+ for (int i = 0; i < attachedDbs.size(); i++) {
+ Pair<String, String> p = attachedDbs.get(i);
+ long pageCount = DatabaseUtils.longForQuery(db, "PRAGMA " + p.first
+ + ".page_count;", null);
+
+ // first entry in the attached db list is always the main database
+ // don't worry about prefixing the dbname with "main"
+ String dbName;
+ if (i == 0) {
+ dbName = lastnode;
+ } else {
+ // lookaside is only relevant for the main db
+ lookasideUsed = 0;
+ dbName = " (attached) " + p.first;
+ // if the attached db has a path, attach the lastnode from the path to above
+ if (p.second.trim().length() > 0) {
+ int idx = p.second.lastIndexOf("/");
+ dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0);
+ }
+ }
+ if (pageCount > 0) {
+ dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(),
+ lookasideUsed, db.mNumCacheHits, db.mNumCacheMisses,
+ db.mCompiledQueries.size()));
}
}
- if (pageCount > 0) {
- dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(),
- lookasideUsed));
+ // if there are pooled connections, return the cache stats for them also.
+ if (db.mConnectionPool != null) {
+ for (SQLiteDatabase pDb : db.mConnectionPool.getConnectionList()) {
+ dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") "
+ + lastnode, 0, 0, 0, pDb.mNumCacheHits, pDb.mNumCacheMisses,
+ pDb.mCompiledQueries.size()));
+ }
}
+ } catch (SQLiteException e) {
+ // ignore. we don't care about exceptions when we are taking adb
+ // bugreport!
}
}
return dbStatsList;
}
/**
- * get the specified pragma value from sqlite for the specified database.
- * only handles pragma's that return int/long.
- * NO JAVA locks are held in this method.
- * TODO: use this to do all pragma's in this class
+ * Returns list of full pathnames of all attached databases including the main database
+ * by executing 'pragma database_list' on the database.
+ *
+ * @return ArrayList of pairs of (database name, database file path) or null if the database
+ * is not open.
*/
- private static long getPragmaVal(SQLiteDatabase db, String pragma) {
- if (!db.isOpen()) {
- return 0;
+ public ArrayList<Pair<String, String>> getAttachedDbs() {
+ if (!isOpen()) {
+ return null;
}
- SQLiteStatement prog = null;
+ ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
+ Cursor c = null;
try {
- prog = new SQLiteStatement(db, "PRAGMA " + pragma);
- long val = prog.simpleQueryForLong();
- return val;
+ c = rawQuery("pragma database_list;", null);
+ while (c.moveToNext()) {
+ // sqlite returns a row for each database in the returned list of databases.
+ // in each row,
+ // 1st column is the database name such as main, or the database
+ // name specified on the "ATTACH" command
+ // 2nd column is the database file path.
+ attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2)));
+ }
} finally {
- if (prog != null) prog.close();
+ if (c != null) {
+ c.close();
+ }
}
+ return attachedDbs;
}
/**
- * returns list of full pathnames of all attached databases
- * including the main database
- * TODO: move this to {@link DatabaseUtils}
- */
- private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) {
- if (!dbObj.isOpen()) {
- return null;
- }
- ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
- Cursor c = dbObj.rawQuery("pragma database_list;", null);
- while (c.moveToNext()) {
- attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2)));
+ * Runs 'pragma integrity_check' on the given database (and all the attached databases)
+ * and returns true if the given database (and all its attached databases) pass integrity_check,
+ * false otherwise.
+ *<p>
+ * If the result is false, then this method logs the errors reported by the integrity_check
+ * command execution.
+ *<p>
+ * Note that 'pragma integrity_check' on a database can take a long time.
+ *
+ * @return true if the given database (and all its attached databases) pass integrity_check,
+ * false otherwise.
+ */
+ public boolean isDatabaseIntegrityOk() {
+ verifyDbIsOpen();
+ ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs();
+ if (attachedDbs == null) {
+ throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " +
+ "be retrieved. probably because the database is closed");
+ }
+ boolean isDatabaseCorrupt = false;
+ for (int i = 0; i < attachedDbs.size(); i++) {
+ Pair<String, String> p = attachedDbs.get(i);
+ SQLiteStatement prog = null;
+ try {
+ prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);");
+ String rslt = prog.simpleQueryForString();
+ if (!rslt.equalsIgnoreCase("ok")) {
+ // integrity_checker failed on main or attached databases
+ isDatabaseCorrupt = true;
+ Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt);
+ }
+ } finally {
+ if (prog != null) prog.close();
+ }
}
- c.close();
- return attachedDbs;
+ return isDatabaseCorrupt;
}
/**
@@ -2239,21 +2599,27 @@ public class SQLiteDatabase extends SQLiteClosable {
private native void dbopen(String path, int flags);
/**
- * Native call to setup tracing of all sql statements
+ * Native call to setup tracing of all SQL statements
*
* @param path the full path to the database
+ * @param connectionNum connection number: 0 - N, where the main database
+ * connection handle is numbered 0 and the connection handles in the connection
+ * pool are numbered 1..N.
*/
- private native void enableSqlTracing(String path);
+ private native void enableSqlTracing(String path, short connectionNum);
/**
- * Native call to setup profiling of all sql statements.
+ * Native call to setup profiling of all SQL statements.
* currently, sqlite's profiling = printing of execution-time
- * (wall-clock time) of each of the sql statements, as they
+ * (wall-clock time) of each of the SQL statements, as they
* are executed.
*
* @param path the full path to the database
+ * @param connectionNum connection number: 0 - N, where the main database
+ * connection handle is numbered 0 and the connection handles in the connection
+ * pool are numbered 1..N.
*/
- private native void enableSqlProfiling(String path);
+ private native void enableSqlProfiling(String path, short connectionNum);
/**
* Native call to execute a raw SQL statement. {@link #lock} must be held
@@ -2291,4 +2657,11 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED
*/
private native int native_getDbLookaside();
+
+ /**
+ * finalizes the given statement id.
+ *
+ * @param statementId statement to be finzlied by sqlite
+ */
+ private final native void native_finalize(int statementId);
}
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index 89c3f96..9496079 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -132,11 +132,16 @@ public final class SQLiteDebug {
/** documented here http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */
public int lookaside;
- public DbStats(String dbName, long pageCount, long pageSize, int lookaside) {
+ /** statement cache stats: hits/misses/cachesize */
+ public String cache;
+
+ public DbStats(String dbName, long pageCount, long pageSize, int lookaside,
+ int hits, int misses, int cachesize) {
this.dbName = dbName;
- this.pageSize = pageSize;
+ this.pageSize = pageSize / 1024;
dbSize = (pageCount * pageSize) / 1024;
this.lookaside = lookaside;
+ this.cache = hits + "/" + misses + "/" + cachesize;
}
}
diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
index 2144fc3..be49257 100644
--- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
+++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -39,9 +39,12 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
public Cursor query(CursorFactory factory, String[] selectionArgs) {
// Compile the query
- SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
+ SQLiteQuery query = null;
try {
+ mDatabase.lock();
+ mDatabase.closePendingStatements();
+ query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
// Arg binding
int numArgs = selectionArgs == null ? 0 : selectionArgs.length;
for (int i = 0; i < numArgs; i++) {
@@ -61,6 +64,7 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
} finally {
// Make sure this object is cleaned up if something happens
if (query != null) query.close();
+ mDatabase.unlock();
}
}
diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java
index aefbabc..0f2e872 100644
--- a/core/java/android/database/sqlite/SQLiteOpenHelper.java
+++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java
@@ -17,6 +17,8 @@
package android.database.sqlite;
import android.content.Context;
+import android.database.DatabaseErrorHandler;
+import android.database.DefaultDatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.util.Log;
@@ -45,6 +47,7 @@ public abstract class SQLiteOpenHelper {
private SQLiteDatabase mDatabase = null;
private boolean mIsInitializing = false;
+ private final DatabaseErrorHandler mErrorHandler;
/**
* Create a helper object to create, open, and/or manage a database.
@@ -58,12 +61,37 @@ public abstract class SQLiteOpenHelper {
* {@link #onUpgrade} will be used to upgrade the database
*/
public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
+ this(context, name, factory, version, new DefaultDatabaseErrorHandler());
+ }
+
+ /**
+ * Create a helper object to create, open, and/or manage a database.
+ * The database is not actually created or opened until one of
+ * {@link #getWritableDatabase} or {@link #getReadableDatabase} is called.
+ *
+ * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
+ * used to handle corruption when sqlite reports database corruption.</p>
+ *
+ * @param context to use to open or create the database
+ * @param name of the database file, or null for an in-memory database
+ * @param factory to use for creating cursor objects, or null for the default
+ * @param version number of the database (starting at 1); if the database is older,
+ * {@link #onUpgrade} will be used to upgrade the database
+ * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database
+ * corruption.
+ */
+ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,
+ DatabaseErrorHandler errorHandler) {
if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
+ if (errorHandler == null) {
+ throw new IllegalArgumentException("DatabaseErrorHandler param value can't be null.");
+ }
mContext = context;
mName = name;
mFactory = factory;
mNewVersion = version;
+ mErrorHandler = errorHandler;
}
/**
@@ -101,10 +129,14 @@ public abstract class SQLiteOpenHelper {
if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
- db = mContext.openOrCreateDatabase(mName, 0, mFactory);
+ db = mContext.openOrCreateDatabase(mName, 0, mFactory, mErrorHandler);
}
int version = db.getVersion();
+ if (version > mNewVersion) {
+ throw new IllegalStateException("Database " + mName +
+ " cannot be downgraded. instead, please uninstall new version first.");
+ }
if (version != mNewVersion) {
db.beginTransaction();
try {
@@ -175,7 +207,8 @@ public abstract class SQLiteOpenHelper {
try {
mIsInitializing = true;
String path = mContext.getDatabasePath(mName).getPath();
- db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
+ db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY,
+ mErrorHandler);
if (db.getVersion() != mNewVersion) {
throw new SQLiteException("Can't upgrade read-only database from version " +
db.getVersion() + " to " + mNewVersion + ": " + path);
diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java
index 4d96f12..017b65f 100644
--- a/core/java/android/database/sqlite/SQLiteProgram.java
+++ b/core/java/android/database/sqlite/SQLiteProgram.java
@@ -17,10 +17,13 @@
package android.database.sqlite;
import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
/**
* A base class for compiled SQLite programs.
- *
+ *<p>
* SQLiteProgram is not internally synchronized so code using a SQLiteProgram from multiple
* threads should perform its own synchronization when using the SQLiteProgram.
*/
@@ -28,6 +31,11 @@ public abstract class SQLiteProgram extends SQLiteClosable {
private static final String TAG = "SQLiteProgram";
+ /** the type of sql statement being processed by this object */
+ /* package */ static final int SELECT_STMT = 1;
+ private static final int UPDATE_STMT = 2;
+ private static final int OTHER_STMT = 3;
+
/** The database this program is compiled against.
* @deprecated do not use this
*/
@@ -58,38 +66,67 @@ public abstract class SQLiteProgram extends SQLiteClosable {
@Deprecated
protected int nStatement = 0;
+ /**
+ * In the case of {@link SQLiteStatement}, this member stores the bindargs passed
+ * to the following methods, instead of actually doing the binding.
+ * <ul>
+ * <li>{@link #bindBlob(int, byte[])}</li>
+ * <li>{@link #bindDouble(int, double)}</li>
+ * <li>{@link #bindLong(int, long)}</li>
+ * <li>{@link #bindNull(int)}</li>
+ * <li>{@link #bindString(int, String)}</li>
+ * </ul>
+ * <p>
+ * Each entry in the array is a Pair of
+ * <ol>
+ * <li>bind arg position number</li>
+ * <li>the value to be bound to the bindarg</li>
+ * </ol>
+ * <p>
+ * It is lazily initialized in the above bind methods
+ * and it is cleared in {@link #clearBindings()} method.
+ * <p>
+ * It is protected (in multi-threaded environment) by {@link SQLiteProgram}.this
+ */
+ private ArrayList<Pair<Integer, Object>> bindArgs = null;
+
/* package */ SQLiteProgram(SQLiteDatabase db, String sql) {
- mDatabase = db;
+ this(db, sql, true);
+ }
+
+ /* package */ SQLiteProgram(SQLiteDatabase db, String sql, boolean compileFlag) {
mSql = sql.trim();
db.acquireReference();
db.addSQLiteClosable(this);
- this.nHandle = db.mNativeHandle;
+ mDatabase = db;
+ nHandle = db.mNativeHandle;
+ if (compileFlag) {
+ compileSql();
+ }
+ }
+ private void compileSql() {
// only cache CRUD statements
- String prefixSql = mSql.substring(0, 6);
- if (!prefixSql.equalsIgnoreCase("INSERT") && !prefixSql.equalsIgnoreCase("UPDATE") &&
- !prefixSql.equalsIgnoreCase("REPLAC") &&
- !prefixSql.equalsIgnoreCase("DELETE") && !prefixSql.equalsIgnoreCase("SELECT")) {
- mCompiledSql = new SQLiteCompiledSql(db, sql);
+ if (getSqlStatementType(mSql) == OTHER_STMT) {
+ mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
nStatement = mCompiledSql.nStatement;
// since it is not in the cache, no need to acquire() it.
return;
}
- // it is not pragma
- mCompiledSql = db.getCompiledStatementForSql(sql);
+ mCompiledSql = mDatabase.getCompiledStatementForSql(mSql);
if (mCompiledSql == null) {
// create a new compiled-sql obj
- mCompiledSql = new SQLiteCompiledSql(db, sql);
+ mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
// add it to the cache of compiled-sqls
// but before adding it and thus making it available for anyone else to use it,
// make sure it is acquired by me.
mCompiledSql.acquire();
- db.addToCompiledQueries(sql, mCompiledSql);
+ mDatabase.addToCompiledQueries(mSql, mCompiledSql);
if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
Log.v(TAG, "Created DbObj (id#" + mCompiledSql.nStatement +
- ") for sql: " + sql);
+ ") for sql: " + mSql);
}
} else {
// it is already in compiled-sql cache.
@@ -100,12 +137,12 @@ public abstract class SQLiteProgram extends SQLiteClosable {
// we can't have two different SQLiteProgam objects can't share the same
// CompiledSql object. create a new one.
// finalize it when I am done with it in "this" object.
- mCompiledSql = new SQLiteCompiledSql(db, sql);
+ mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
Log.v(TAG, "** possible bug ** Created NEW DbObj (id#" +
mCompiledSql.nStatement +
") because the previously created DbObj (id#" + last +
- ") was not released for sql:" + sql);
+ ") was not released for sql:" + mSql);
}
// since it is not in the cache, no need to acquire() it.
}
@@ -113,11 +150,27 @@ public abstract class SQLiteProgram extends SQLiteClosable {
nStatement = mCompiledSql.nStatement;
}
+ /* package */ int getSqlStatementType(String sql) {
+ if (mSql.length() < 6) {
+ return OTHER_STMT;
+ }
+ String prefixSql = mSql.substring(0, 6);
+ if (prefixSql.equalsIgnoreCase("SELECT")) {
+ return SELECT_STMT;
+ } else if (prefixSql.equalsIgnoreCase("INSERT") ||
+ prefixSql.equalsIgnoreCase("UPDATE") ||
+ prefixSql.equalsIgnoreCase("REPLAC") ||
+ prefixSql.equalsIgnoreCase("DELETE")) {
+ return UPDATE_STMT;
+ }
+ return OTHER_STMT;
+ }
+
@Override
protected void onAllReferencesReleased() {
releaseCompiledSqlIfNotInCache();
- mDatabase.releaseReference();
mDatabase.removeSQLiteClosable(this);
+ mDatabase.releaseReference();
}
@Override
@@ -126,7 +179,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
mDatabase.releaseReference();
}
- private void releaseCompiledSqlIfNotInCache() {
+ /* package */ synchronized void releaseCompiledSqlIfNotInCache() {
if (mCompiledSql == null) {
return;
}
@@ -135,22 +188,34 @@ public abstract class SQLiteProgram extends SQLiteClosable {
// it is NOT in compiled-sql cache. i.e., responsibility of
// releasing this statement is on me.
mCompiledSql.releaseSqlStatement();
- mCompiledSql = null;
- nStatement = 0;
} else {
// it is in compiled-sql cache. reset its CompiledSql#mInUse flag
mCompiledSql.release();
}
- }
+ }
+ mCompiledSql = null;
+ nStatement = 0;
}
/**
* Returns a unique identifier for this program.
*
* @return a unique identifier for this program
+ * @deprecated do not use this method. it is not guaranteed to be the same across executions of
+ * the SQL statement contained in this object.
*/
+ @Deprecated
public final int getUniqueId() {
- return nStatement;
+ return -1;
+ }
+
+ /**
+ * used only for testing purposes
+ */
+ /* package */ int getSqlStatementId() {
+ synchronized(this) {
+ return (mCompiledSql == null) ? 0 : nStatement;
+ }
}
/* package */ String getSqlString() {
@@ -176,14 +241,20 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param index The 1-based index to the parameter to bind null to
*/
public void bindNull(int index) {
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- acquireReference();
- try {
- native_bind_null(index);
- } finally {
- releaseReference();
+ mDatabase.verifyDbIsOpen();
+ synchronized (this) {
+ acquireReference();
+ try {
+ if (this.nStatement == 0) {
+ // since the SQL statement is not compiled, don't do the binding yet.
+ // can be done before executing the SQL statement
+ addToBindArgs(index, null);
+ } else {
+ native_bind_null(index);
+ }
+ } finally {
+ releaseReference();
+ }
}
}
@@ -195,14 +266,18 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param value The value to bind
*/
public void bindLong(int index, long value) {
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- acquireReference();
- try {
- native_bind_long(index, value);
- } finally {
- releaseReference();
+ mDatabase.verifyDbIsOpen();
+ synchronized (this) {
+ acquireReference();
+ try {
+ if (this.nStatement == 0) {
+ addToBindArgs(index, value);
+ } else {
+ native_bind_long(index, value);
+ }
+ } finally {
+ releaseReference();
+ }
}
}
@@ -214,14 +289,18 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param value The value to bind
*/
public void bindDouble(int index, double value) {
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- acquireReference();
- try {
- native_bind_double(index, value);
- } finally {
- releaseReference();
+ mDatabase.verifyDbIsOpen();
+ synchronized (this) {
+ acquireReference();
+ try {
+ if (this.nStatement == 0) {
+ addToBindArgs(index, value);
+ } else {
+ native_bind_double(index, value);
+ }
+ } finally {
+ releaseReference();
+ }
}
}
@@ -236,14 +315,18 @@ public abstract class SQLiteProgram extends SQLiteClosable {
if (value == null) {
throw new IllegalArgumentException("the bind value at index " + index + " is null");
}
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- acquireReference();
- try {
- native_bind_string(index, value);
- } finally {
- releaseReference();
+ mDatabase.verifyDbIsOpen();
+ synchronized (this) {
+ acquireReference();
+ try {
+ if (this.nStatement == 0) {
+ addToBindArgs(index, value);
+ } else {
+ native_bind_string(index, value);
+ }
+ } finally {
+ releaseReference();
+ }
}
}
@@ -258,14 +341,18 @@ public abstract class SQLiteProgram extends SQLiteClosable {
if (value == null) {
throw new IllegalArgumentException("the bind value at index " + index + " is null");
}
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- acquireReference();
- try {
- native_bind_blob(index, value);
- } finally {
- releaseReference();
+ mDatabase.verifyDbIsOpen();
+ synchronized (this) {
+ acquireReference();
+ try {
+ if (this.nStatement == 0) {
+ addToBindArgs(index, value);
+ } else {
+ native_bind_blob(index, value);
+ }
+ } finally {
+ releaseReference();
+ }
}
}
@@ -273,14 +360,18 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* Clears all existing bindings. Unset bindings are treated as NULL.
*/
public void clearBindings() {
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- acquireReference();
- try {
- native_clear_bindings();
- } finally {
- releaseReference();
+ synchronized (this) {
+ bindArgs = null;
+ if (this.nStatement == 0) {
+ return;
+ }
+ mDatabase.verifyDbIsOpen();
+ acquireReference();
+ try {
+ native_clear_bindings();
+ } finally {
+ releaseReference();
+ }
}
}
@@ -288,14 +379,40 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* Release this program's resources, making it invalid.
*/
public void close() {
- if (!mDatabase.isOpen()) {
+ synchronized (this) {
+ bindArgs = null;
+ if (nHandle == 0 || !mDatabase.isOpen()) {
+ return;
+ }
+ releaseReference();
+ }
+ }
+
+ private synchronized void addToBindArgs(int index, Object value) {
+ if (bindArgs == null) {
+ bindArgs = new ArrayList<Pair<Integer, Object>>();
+ }
+ bindArgs.add(new Pair<Integer, Object>(index, value));
+ }
+
+ /* package */ synchronized void compileAndbindAllArgs() {
+ assert nStatement == 0;
+ compileSql();
+ if (bindArgs == null) {
return;
}
- mDatabase.lock();
- try {
- releaseReference();
- } finally {
- mDatabase.unlock();
+ for (Pair<Integer, Object> p : bindArgs) {
+ if (p.second == null) {
+ native_bind_null(p.first);
+ } else if (p.second instanceof Long) {
+ native_bind_long(p.first, (Long)p.second);
+ } else if (p.second instanceof Double) {
+ native_bind_double(p.first, (Double)p.second);
+ } else if (p.second instanceof byte[]) {
+ native_bind_blob(p.first, (byte[])p.second);
+ } else {
+ native_bind_string(p.first, (String)p.second);
+ }
}
}
@@ -320,6 +437,6 @@ public abstract class SQLiteProgram extends SQLiteClosable {
protected final native void native_bind_double(int index, double value);
protected final native void native_bind_string(int index, String value);
protected final native void native_bind_blob(int index, byte[] value);
- private final native void native_clear_bindings();
+ /* package */ final native void native_clear_bindings();
}
diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java
index 905b66b..e6011ee 100644
--- a/core/java/android/database/sqlite/SQLiteQuery.java
+++ b/core/java/android/database/sqlite/SQLiteQuery.java
@@ -72,11 +72,6 @@ public class SQLiteQuery extends SQLiteProgram {
// is not safe in this situation. the native code will ignore maxRead
int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex,
maxRead, lastPos);
-
- // Logging
- if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
- Log.d(TAG, "fillWindow(): " + mSql);
- }
mDatabase.logTimeStat(mSql, timeStart);
return numRows;
} catch (IllegalStateException e){
diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
index 9e425c3..b902803 100644
--- a/core/java/android/database/sqlite/SQLiteStatement.java
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -25,12 +25,18 @@ import dalvik.system.BlockGuard;
* The statement cannot return multiple rows, but 1x1 result sets are allowed.
* Don't use SQLiteStatement constructor directly, please use
* {@link SQLiteDatabase#compileStatement(String)}
- *
+ *<p>
* SQLiteStatement is not internally synchronized so code using a SQLiteStatement from multiple
* threads should perform its own synchronization when using the SQLiteStatement.
*/
+@SuppressWarnings("deprecation")
public class SQLiteStatement extends SQLiteProgram
{
+ private static final boolean READ = true;
+ private static final boolean WRITE = false;
+
+ private SQLiteDatabase mOrigDb;
+
/**
* Don't use SQLiteStatement constructor directly, please use
* {@link SQLiteDatabase#compileStatement(String)}
@@ -38,7 +44,7 @@ public class SQLiteStatement extends SQLiteProgram
* @param sql
*/
/* package */ SQLiteStatement(SQLiteDatabase db, String sql) {
- super(db, sql);
+ super(db, sql, false /* don't compile sql statement */);
}
/**
@@ -49,20 +55,14 @@ public class SQLiteStatement extends SQLiteProgram
* some reason
*/
public void execute() {
- BlockGuard.getThreadPolicy().onWriteToDisk();
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- long timeStart = SystemClock.uptimeMillis();
- mDatabase.lock();
-
- acquireReference();
- try {
- native_execute();
- mDatabase.logTimeStat(mSql, timeStart);
- } finally {
- releaseReference();
- mDatabase.unlock();
+ synchronized(this) {
+ long timeStart = acquireAndLock(WRITE);
+ try {
+ native_execute();
+ mDatabase.logTimeStat(mSql, timeStart);
+ } finally {
+ releaseAndUnlock();
+ }
}
}
@@ -76,21 +76,15 @@ public class SQLiteStatement extends SQLiteProgram
* some reason
*/
public long executeInsert() {
- BlockGuard.getThreadPolicy().onWriteToDisk();
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- long timeStart = SystemClock.uptimeMillis();
- mDatabase.lock();
-
- acquireReference();
- try {
- native_execute();
- mDatabase.logTimeStat(mSql, timeStart);
- return (mDatabase.lastChangeCount() > 0) ? mDatabase.lastInsertRow() : -1;
- } finally {
- releaseReference();
- mDatabase.unlock();
+ synchronized(this) {
+ long timeStart = acquireAndLock(WRITE);
+ try {
+ native_execute();
+ mDatabase.logTimeStat(mSql, timeStart);
+ return (mDatabase.lastChangeCount() > 0) ? mDatabase.lastInsertRow() : -1;
+ } finally {
+ releaseAndUnlock();
+ }
}
}
@@ -103,21 +97,15 @@ public class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public long simpleQueryForLong() {
- BlockGuard.getThreadPolicy().onReadFromDisk();
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
- }
- long timeStart = SystemClock.uptimeMillis();
- mDatabase.lock();
-
- acquireReference();
- try {
- long retValue = native_1x1_long();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } finally {
- releaseReference();
- mDatabase.unlock();
+ synchronized(this) {
+ long timeStart = acquireAndLock(READ);
+ try {
+ long retValue = native_1x1_long();
+ mDatabase.logTimeStat(mSql, timeStart);
+ return retValue;
+ } finally {
+ releaseAndUnlock();
+ }
}
}
@@ -130,22 +118,68 @@ public class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public String simpleQueryForString() {
- BlockGuard.getThreadPolicy().onReadFromDisk();
- if (!mDatabase.isOpen()) {
- throw new IllegalStateException("database " + mDatabase.getPath() + " already closed");
+ synchronized(this) {
+ long timeStart = acquireAndLock(READ);
+ try {
+ String retValue = native_1x1_string();
+ mDatabase.logTimeStat(mSql, timeStart);
+ return retValue;
+ } finally {
+ releaseAndUnlock();
+ }
}
- long timeStart = SystemClock.uptimeMillis();
- mDatabase.lock();
+ }
- acquireReference();
- try {
- String retValue = native_1x1_string();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } finally {
- releaseReference();
- mDatabase.unlock();
+ /**
+ * Called before every method in this class before executing a SQL statement,
+ * this method does the following:
+ * <ul>
+ * <li>make sure the database is open</li>
+ * <li>get a database connection from the connection pool,if possible</li>
+ * <li>notifies {@link BlockGuard} of read/write</li>
+ * <li>get lock on the database</li>
+ * <li>acquire reference on this object</li>
+ * <li>and then return the current time _before_ the database lock was acquired</li>
+ * </ul>
+ * <p>
+ * This method removes the duplcate code from the other public
+ * methods in this class.
+ */
+ private long acquireAndLock(boolean rwFlag) {
+ // use pooled database connection handles for SELECT SQL statements
+ mDatabase.verifyDbIsOpen();
+ SQLiteDatabase db = (getSqlStatementType(mSql) != SELECT_STMT) ? mDatabase
+ : mDatabase.getDbConnection(mSql);
+ // use the database connection obtained above
+ mOrigDb = mDatabase;
+ mDatabase = db;
+ nHandle = mDatabase.mNativeHandle;
+ if (rwFlag == WRITE) {
+ BlockGuard.getThreadPolicy().onWriteToDisk();
+ } else {
+ BlockGuard.getThreadPolicy().onReadFromDisk();
}
+ long startTime = SystemClock.uptimeMillis();
+ mDatabase.lock();
+ acquireReference();
+ mDatabase.closePendingStatements();
+ compileAndbindAllArgs();
+ return startTime;
+ }
+
+ /**
+ * this method releases locks and references acquired in {@link #acquireAndLock(boolean)}.
+ */
+ private void releaseAndUnlock() {
+ releaseReference();
+ mDatabase.unlock();
+ clearBindings();
+ // release the compiled sql statement so that the caller's SQLiteStatement no longer
+ // has a hard reference to a database object that may get deallocated at any point.
+ releaseCompiledSqlIfNotInCache();
+ // restore the database connection handle to the original value
+ mDatabase = mOrigDb;
+ nHandle = mDatabase.mNativeHandle;
}
private final native void native_execute();
diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java
index 70519ff..aaf3898 100644
--- a/core/java/android/hardware/SensorEvent.java
+++ b/core/java/android/hardware/SensorEvent.java
@@ -84,14 +84,14 @@ public class SensorEvent {
* sensor itself (<b>Fs</b>) using the relation:
* </p>
*
- * <b><center>Ad = - ·Fs / mass</center></b>
+ * <b><center>Ad = - &#8721;Fs / mass</center></b>
*
* <p>
* In particular, the force of gravity is always influencing the measured
* acceleration:
* </p>
*
- * <b><center>Ad = -g - ·F / mass</center></b>
+ * <b><center>Ad = -g - &#8721;F / mass</center></b>
*
* <p>
* For this reason, when the device is sitting on a table (and obviously not
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 280ded6..6335296 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -524,5 +524,20 @@ public class ConnectivityManager
} catch (RemoteException e) {
return TETHER_ERROR_SERVICE_UNAVAIL;
}
- }
+ }
+
+ /**
+ * Ensure the device stays awake until we connect with the next network
+ * @param forWhome The name of the network going down for logging purposes
+ * @return {@code true} on success, {@code false} on failure
+ * {@hide}
+ */
+ public boolean requestNetworkTransitionWakelock(String forWhom) {
+ try {
+ mService.requestNetworkTransitionWakelock(forWhom);
+ return true;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
}
diff --git a/core/java/android/net/Downloads.java b/core/java/android/net/Downloads.java
index fd33781..ddde5c1 100644
--- a/core/java/android/net/Downloads.java
+++ b/core/java/android/net/Downloads.java
@@ -430,11 +430,10 @@ public final class Downloads {
ContentResolver cr = context.getContentResolver();
- Cursor c = cr.query(
- downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */,
- null /* sort order */);
+ Cursor c = cr.query(downloadUri, DOWNLOADS_PROJECTION, null /* selection */,
+ null /* selection args */, null /* sort order */);
try {
- if (!c.moveToNext()) {
+ if (c == null || !c.moveToNext()) {
return result;
}
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index b05c2ed..5a14cc9 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -72,4 +72,6 @@ interface IConnectivityManager
String[] getTetherableUsbRegexs();
String[] getTetherableWifiRegexs();
+
+ void requestNetworkTransitionWakelock(in String forWhom);
}
diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java
index 214510d..e74db67 100644
--- a/core/java/android/net/MobileDataStateTracker.java
+++ b/core/java/android/net/MobileDataStateTracker.java
@@ -22,12 +22,14 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.RemoteException;
import android.os.Handler;
+import android.os.Message;
import android.os.ServiceManager;
-import android.os.SystemProperties;
import com.android.internal.telephony.ITelephony;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.TelephonyIntents;
import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkInfo;
+import android.net.NetworkProperties;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.text.TextUtils;
@@ -39,7 +41,7 @@ import android.text.TextUtils;
*
* {@hide}
*/
-public class MobileDataStateTracker extends NetworkStateTracker {
+public class MobileDataStateTracker implements NetworkStateTracker {
private static final String TAG = "MobileDataStateTracker";
private static final boolean DBG = true;
@@ -48,10 +50,16 @@ public class MobileDataStateTracker extends NetworkStateTracker {
private ITelephony mPhoneService;
private String mApnType;
- private String mApnTypeToWatchFor;
- private String mApnName;
- private boolean mEnabled;
private BroadcastReceiver mStateReceiver;
+ private static String[] sDnsPropNames;
+ private NetworkInfo mNetworkInfo;
+ private boolean mTeardownRequested = false;
+ private Handler mTarget;
+ private Context mContext;
+ private NetworkProperties mNetworkProperties;
+ private boolean mPrivateDnsRouteSet = false;
+ private int mDefaultGatewayAddr = 0;
+ private boolean mDefaultRouteSet = false;
/**
* Create a new MobileDataStateTracker
@@ -62,24 +70,16 @@ public class MobileDataStateTracker extends NetworkStateTracker {
* @param tag the name of this network
*/
public MobileDataStateTracker(Context context, Handler target, int netType, String tag) {
- super(context, target, netType,
+ mTarget = target;
+ mContext = context;
+ mNetworkInfo = new NetworkInfo(netType,
TelephonyManager.getDefault().getNetworkType(), tag,
TelephonyManager.getDefault().getNetworkTypeName());
mApnType = networkTypeToApnType(netType);
- if (TextUtils.equals(mApnType, Phone.APN_TYPE_HIPRI)) {
- mApnTypeToWatchFor = Phone.APN_TYPE_DEFAULT;
- } else {
- mApnTypeToWatchFor = mApnType;
- }
mPhoneService = null;
- if(netType == ConnectivityManager.TYPE_MOBILE) {
- mEnabled = true;
- } else {
- mEnabled = false;
- }
- mDnsPropNames = new String[] {
+ sDnsPropNames = new String[] {
"net.rmnet0.dns1",
"net.rmnet0.dns2",
"net.eth0.dns1",
@@ -94,6 +94,45 @@ public class MobileDataStateTracker extends NetworkStateTracker {
}
/**
+ * Return the IP addresses of the DNS servers available for the mobile data
+ * network interface.
+ * @return a list of DNS addresses, with no holes.
+ */
+ public String[] getDnsPropNames() {
+ return sDnsPropNames;
+ }
+
+ public boolean isPrivateDnsRouteSet() {
+ return mPrivateDnsRouteSet;
+ }
+
+ public void privateDnsRouteSet(boolean enabled) {
+ mPrivateDnsRouteSet = enabled;
+ }
+
+ public NetworkInfo getNetworkInfo() {
+ return mNetworkInfo;
+ }
+
+ public int getDefaultGatewayAddr() {
+ return mDefaultGatewayAddr;
+ }
+
+ public boolean isDefaultRouteSet() {
+ return mDefaultRouteSet;
+ }
+
+ public void defaultRouteSet(boolean enabled) {
+ mDefaultRouteSet = enabled;
+ }
+
+ /**
+ * This is not implemented.
+ */
+ public void releaseWakeLock() {
+ }
+
+ /**
* Begin monitoring mobile data connectivity.
*/
public void startMonitoring() {
@@ -103,38 +142,34 @@ public class MobileDataStateTracker extends NetworkStateTracker {
filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED);
mStateReceiver = new MobileDataStateReceiver();
- Intent intent = mContext.registerReceiver(mStateReceiver, filter);
- if (intent != null)
- mMobileDataState = getMobileDataState(intent);
- else
- mMobileDataState = Phone.DataState.DISCONNECTED;
- }
-
- private Phone.DataState getMobileDataState(Intent intent) {
- String str = intent.getStringExtra(Phone.STATE_KEY);
- if (str != null) {
- String apnTypeList =
- intent.getStringExtra(Phone.DATA_APN_TYPES_KEY);
- if (isApnTypeIncluded(apnTypeList)) {
- return Enum.valueOf(Phone.DataState.class, str);
- }
- }
- return Phone.DataState.DISCONNECTED;
+ mContext.registerReceiver(mStateReceiver, filter);
+ mMobileDataState = Phone.DataState.DISCONNECTED;
}
- private boolean isApnTypeIncluded(String typeList) {
- /* comma seperated list - split and check */
- if (typeList == null)
- return false;
+ /**
+ * Record the roaming status of the device, and if it is a change from the previous
+ * status, send a notification to any listeners.
+ * @param isRoaming {@code true} if the device is now roaming, {@code false}
+ * if it is no longer roaming.
+ */
+ private void setRoamingStatus(boolean isRoaming) {
+ if (isRoaming != mNetworkInfo.isRoaming()) {
+ mNetworkInfo.setRoaming(isRoaming);
+ Message msg = mTarget.obtainMessage(EVENT_ROAMING_CHANGED, mNetworkInfo);
+ msg.sendToTarget();
+ }
+ }
- String[] list = typeList.split(",");
- for(int i=0; i< list.length; i++) {
- if (TextUtils.equals(list[i], mApnTypeToWatchFor) ||
- TextUtils.equals(list[i], Phone.APN_TYPE_ALL)) {
- return true;
+ private void setSubtype(int subtype, String subtypeName) {
+ if (mNetworkInfo.isConnected()) {
+ int oldSubtype = mNetworkInfo.getSubtype();
+ if (subtype != oldSubtype) {
+ mNetworkInfo.setSubtype(subtype, subtypeName);
+ Message msg = mTarget.obtainMessage(
+ EVENT_NETWORK_SUBTYPE_CHANGED, oldSubtype, 0, mNetworkInfo);
+ msg.sendToTarget();
}
}
- return false;
}
private class MobileDataStateReceiver extends BroadcastReceiver {
@@ -142,57 +177,38 @@ public class MobileDataStateTracker extends NetworkStateTracker {
synchronized(this) {
if (intent.getAction().equals(TelephonyIntents.
ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) {
- Phone.DataState state = getMobileDataState(intent);
+ String apnType = intent.getStringExtra(Phone.DATA_APN_TYPE_KEY);
+
+ if (!TextUtils.equals(apnType, mApnType)) {
+ return;
+ }
+ Phone.DataState state = Enum.valueOf(Phone.DataState.class,
+ intent.getStringExtra(Phone.STATE_KEY));
String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY);
String apnName = intent.getStringExtra(Phone.DATA_APN_KEY);
- String apnTypeList = intent.getStringExtra(Phone.DATA_APN_TYPES_KEY);
- mApnName = apnName;
boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY,
false);
-
- // set this regardless of the apnTypeList. It's all the same radio/network
- // underneath
mNetworkInfo.setIsAvailable(!unavailable);
- if (isApnTypeIncluded(apnTypeList)) {
- if (mEnabled == false) {
- // if we're not enabled but the APN Type is supported by this connection
- // we should record the interface name if one's provided. If the user
- // turns on this network we will need the interfacename but won't get
- // a fresh connected message - TODO fix this when we get per-APN
- // notifications
- if (state == Phone.DataState.CONNECTED) {
- if (DBG) Log.d(TAG, "replacing old mInterfaceName (" +
- mInterfaceName + ") with " +
- intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY) +
- " for " + mApnType);
- mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY);
- }
- return;
- }
- } else {
- return;
- }
-
if (DBG) Log.d(TAG, mApnType + " Received state= " + state + ", old= " +
mMobileDataState + ", reason= " +
- (reason == null ? "(unspecified)" : reason) +
- ", apnTypeList= " + apnTypeList);
+ (reason == null ? "(unspecified)" : reason));
if (mMobileDataState != state) {
mMobileDataState = state;
switch (state) {
case DISCONNECTED:
if(isTeardownRequested()) {
- mEnabled = false;
setTeardownRequested(false);
}
setDetailedState(DetailedState.DISCONNECTED, reason, apnName);
- if (mInterfaceName != null) {
- NetworkUtils.resetConnections(mInterfaceName);
+ if (mNetworkProperties != null) {
+ NetworkUtils.resetConnections(mNetworkProperties.getInterface().
+ getName());
}
+ // TODO - check this
// can't do this here - ConnectivityService needs it to clear stuff
// it's ok though - just leave it to be refreshed next time
// we connect.
@@ -208,9 +224,11 @@ public class MobileDataStateTracker extends NetworkStateTracker {
setDetailedState(DetailedState.SUSPENDED, reason, apnName);
break;
case CONNECTED:
- mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY);
- if (mInterfaceName == null) {
- Log.d(TAG, "CONNECTED event did not supply interface name.");
+ mNetworkProperties = intent.getParcelableExtra(
+ Phone.DATA_NETWORK_PROPERTIES_KEY);
+ if (mNetworkProperties == null) {
+ Log.d(TAG,
+ "CONNECTED event did not supply network properties.");
}
setDetailedState(DetailedState.CONNECTED, reason, apnName);
break;
@@ -218,11 +236,14 @@ public class MobileDataStateTracker extends NetworkStateTracker {
}
} else if (intent.getAction().
equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) {
- mEnabled = false;
+ String apnType = intent.getStringExtra(Phone.DATA_APN_TYPE_KEY);
+ if (!TextUtils.equals(apnType, mApnType)) {
+ return;
+ }
String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY);
String apnName = intent.getStringExtra(Phone.DATA_APN_KEY);
- if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" +
- reason == null ? "" : "(" + reason + ")");
+ if (DBG) Log.d(TAG, mApnType + "Received " + intent.getAction() +
+ " broadcast" + reason == null ? "" : "(" + reason + ")");
setDetailedState(DetailedState.FAILED, reason, apnName);
}
TelephonyManager tm = TelephonyManager.getDefault();
@@ -320,70 +341,88 @@ public class MobileDataStateTracker extends NetworkStateTracker {
/**
* Tear down mobile data connectivity, i.e., disable the ability to create
* mobile data connections.
+ * TODO - make async and return nothing?
*/
- @Override
public boolean teardown() {
- // since we won't get a notification currently (TODO - per APN notifications)
- // we won't get a disconnect message until all APN's on the current connection's
- // APN list are disabled. That means privateRoutes for DNS and such will remain on -
- // not a problem since that's all shared with whatever other APN is still on, but
- // ugly.
setTeardownRequested(true);
return (setEnableApn(mApnType, false) != Phone.APN_REQUEST_FAILED);
}
/**
+ * Record the detailed state of a network, and if it is a
+ * change from the previous state, send a notification to
+ * any listeners.
+ * @param state the new @{code DetailedState}
+ */
+ private void setDetailedState(NetworkInfo.DetailedState state) {
+ setDetailedState(state, null, null);
+ }
+
+ /**
+ * Record the detailed state of a network, and if it is a
+ * change from the previous state, send a notification to
+ * any listeners.
+ * @param state the new @{code DetailedState}
+ * @param reason a {@code String} indicating a reason for the state change,
+ * if one was supplied. May be {@code null}.
+ * @param extraInfo optional {@code String} providing extra information about the state change
+ */
+ private void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) {
+ if (DBG) Log.d(TAG, "setDetailed state, old ="
+ + mNetworkInfo.getDetailedState() + " and new state=" + state);
+ if (state != mNetworkInfo.getDetailedState()) {
+ boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING);
+ String lastReason = mNetworkInfo.getReason();
+ /*
+ * If a reason was supplied when the CONNECTING state was entered, and no
+ * reason was supplied for entering the CONNECTED state, then retain the
+ * reason that was supplied when going to CONNECTING.
+ */
+ if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null
+ && lastReason != null)
+ reason = lastReason;
+ mNetworkInfo.setDetailedState(state, reason, extraInfo);
+ Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo);
+ msg.sendToTarget();
+ }
+ }
+
+ private void setDetailedStateInternal(NetworkInfo.DetailedState state) {
+ mNetworkInfo.setDetailedState(state, null, null);
+ }
+
+ public void setTeardownRequested(boolean isRequested) {
+ mTeardownRequested = isRequested;
+ }
+
+ public boolean isTeardownRequested() {
+ return mTeardownRequested;
+ }
+
+ /**
* Re-enable mobile data connectivity after a {@link #teardown()}.
+ * TODO - make async and always get a notification?
*/
public boolean reconnect() {
+ boolean retValue = false; //connected or expect to be?
setTeardownRequested(false);
switch (setEnableApn(mApnType, true)) {
case Phone.APN_ALREADY_ACTIVE:
- // TODO - remove this when we get per-apn notifications
- mEnabled = true;
// need to set self to CONNECTING so the below message is handled.
- mMobileDataState = Phone.DataState.CONNECTING;
- setDetailedState(DetailedState.CONNECTING, Phone.REASON_APN_CHANGED, null);
- //send out a connected message
- Intent intent = new Intent(TelephonyIntents.
- ACTION_ANY_DATA_CONNECTION_STATE_CHANGED);
- intent.putExtra(Phone.STATE_KEY, Phone.DataState.CONNECTED.toString());
- intent.putExtra(Phone.STATE_CHANGE_REASON_KEY, Phone.REASON_APN_CHANGED);
- intent.putExtra(Phone.DATA_APN_TYPES_KEY, mApnTypeToWatchFor);
- intent.putExtra(Phone.DATA_APN_KEY, mApnName);
- intent.putExtra(Phone.DATA_IFACE_NAME_KEY, mInterfaceName);
- intent.putExtra(Phone.NETWORK_UNAVAILABLE_KEY, false);
- if (mStateReceiver != null) mStateReceiver.onReceive(mContext, intent);
+ retValue = true;
break;
case Phone.APN_REQUEST_STARTED:
- mEnabled = true;
// no need to do anything - we're already due some status update intents
+ retValue = true;
break;
case Phone.APN_REQUEST_FAILED:
- if (mPhoneService == null && mApnType == Phone.APN_TYPE_DEFAULT) {
- // on startup we may try to talk to the phone before it's ready
- // since the phone will come up enabled, go with that.
- // TODO - this also comes up on telephony crash: if we think mobile data is
- // off and the telephony stuff crashes and has to restart it will come up
- // enabled (making a data connection). We will then be out of sync.
- // A possible solution is a broadcast when telephony restarts.
- mEnabled = true;
- return false;
- }
- // else fall through
case Phone.APN_TYPE_NOT_AVAILABLE:
- // Default is always available, but may be off due to
- // AirplaneMode or E-Call or whatever..
- if (mApnType != Phone.APN_TYPE_DEFAULT) {
- mEnabled = false;
- }
break;
default:
Log.e(TAG, "Error in reconnect - unexpected response.");
- mEnabled = false;
break;
}
- return mEnabled;
+ return retValue;
}
/**
@@ -457,23 +496,9 @@ public class MobileDataStateTracker extends NetworkStateTracker {
}
/**
- * Ensure that a network route exists to deliver traffic to the specified
- * host via the mobile data network.
- * @param hostAddress the IP address of the host to which the route is desired,
- * in network byte order.
- * @return {@code true} on success, {@code false} on failure
+ * This is not supported.
*/
- @Override
- public boolean requestRouteToHost(int hostAddress) {
- if (DBG) {
- Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress) +
- " for " + mApnType + "(" + mInterfaceName + ")");
- }
- if (mInterfaceName != null && hostAddress != -1) {
- return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0;
- } else {
- return false;
- }
+ public void interpretScanResultsAvailable() {
}
@Override
@@ -537,4 +562,8 @@ public class MobileDataStateTracker extends NetworkStateTracker {
return null;
}
}
+
+ public NetworkProperties getNetworkProperties() {
+ return mNetworkProperties;
+ }
}
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
index 649cb8c..21f711c 100644
--- a/core/java/android/net/NetworkInfo.java
+++ b/core/java/android/net/NetworkInfo.java
@@ -121,7 +121,10 @@ public class NetworkInfo implements Parcelable {
*/
public NetworkInfo(int type) {}
- NetworkInfo(int type, int subtype, String typeName, String subtypeName) {
+ /**
+ * @hide
+ */
+ public NetworkInfo(int type, int subtype, String typeName, String subtypeName) {
if (!ConnectivityManager.isNetworkTypeValid(type)) {
throw new IllegalArgumentException("Invalid network type: " + type);
}
@@ -281,8 +284,9 @@ public class NetworkInfo implements Parcelable {
* if one was supplied. May be {@code null}.
* @param extraInfo an optional {@code String} providing addditional network state
* information passed up from the lower networking layers.
+ * @hide
*/
- void setDetailedState(DetailedState detailedState, String reason, String extraInfo) {
+ public void setDetailedState(DetailedState detailedState, String reason, String extraInfo) {
this.mDetailedState = detailedState;
this.mState = stateMap.get(detailedState);
this.mReason = reason;
diff --git a/core/java/android/net/NetworkProperties.aidl b/core/java/android/net/NetworkProperties.aidl
new file mode 100644
index 0000000..07aac6e
--- /dev/null
+++ b/core/java/android/net/NetworkProperties.aidl
@@ -0,0 +1,22 @@
+/*
+**
+** Copyright (C) 2009 Qualcomm Innovation Center, Inc. All Rights Reserved.
+** 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.net;
+
+parcelable NetworkProperties;
+
diff --git a/core/java/android/net/NetworkProperties.java b/core/java/android/net/NetworkProperties.java
new file mode 100644
index 0000000..56e1f1a
--- /dev/null
+++ b/core/java/android/net/NetworkProperties.java
@@ -0,0 +1,196 @@
+/*
+ * 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.net;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Describes the properties of a network interface or single address
+ * of an interface.
+ * TODO - consider adding optional fields like Apn and ApnType
+ * @hide
+ */
+public class NetworkProperties implements Parcelable {
+
+ private NetworkInterface mIface;
+ private Collection<InetAddress> mAddresses;
+ private Collection<InetAddress> mDnses;
+ private InetAddress mGateway;
+ private ProxyProperties mHttpProxy;
+
+ public NetworkProperties() {
+ clear();
+ }
+
+ public synchronized void setInterface(NetworkInterface iface) {
+ mIface = iface;
+ }
+ public synchronized NetworkInterface getInterface() {
+ return mIface;
+ }
+ public synchronized String getInterfaceName() {
+ return (mIface == null ? null : mIface.getName());
+ }
+
+ public synchronized void addAddress(InetAddress address) {
+ mAddresses.add(address);
+ }
+ public synchronized Collection<InetAddress> getAddresses() {
+ return mAddresses;
+ }
+
+ public synchronized void addDns(InetAddress dns) {
+ mDnses.add(dns);
+ }
+ public synchronized Collection<InetAddress> getDnses() {
+ return mDnses;
+ }
+
+ public synchronized void setGateway(InetAddress gateway) {
+ mGateway = gateway;
+ }
+ public synchronized InetAddress getGateway() {
+ return mGateway;
+ }
+
+ public synchronized void setHttpProxy(ProxyProperties proxy) {
+ mHttpProxy = proxy;
+ }
+ public synchronized ProxyProperties getHttpProxy() {
+ return mHttpProxy;
+ }
+
+ public synchronized void clear() {
+ mIface = null;
+ mAddresses = new ArrayList<InetAddress>();
+ mDnses = new ArrayList<InetAddress>();
+ mGateway = null;
+ mHttpProxy = null;
+ }
+
+ /**
+ * Implement the Parcelable interface
+ * @hide
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ public synchronized String toString() {
+ String ifaceName = (mIface == null ? "" : "InterfaceName: " + mIface.getName() + " ");
+
+ String ip = "IpAddresses: [";
+ for (InetAddress addr : mAddresses) ip += addr.toString() + ",";
+ ip += "] ";
+
+ String dns = "DnsAddresses: [";
+ for (InetAddress addr : mDnses) dns += addr.toString() + ",";
+ dns += "] ";
+
+ String proxy = (mHttpProxy == null ? "" : "HttpProxy: " + mHttpProxy.toString() + " ");
+ String gateway = (mGateway == null ? "" : "Gateway: " + mGateway.toString() + " ");
+
+ return ifaceName + ip + gateway + dns + proxy;
+ }
+
+ /**
+ * Implement the Parcelable interface.
+ * @hide
+ */
+ public synchronized void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(getInterfaceName());
+ dest.writeInt(mAddresses.size());
+ for(InetAddress a : mAddresses) {
+ dest.writeString(a.getHostName());
+ dest.writeByteArray(a.getAddress());
+ }
+ dest.writeInt(mDnses.size());
+ for(InetAddress d : mDnses) {
+ dest.writeString(d.getHostName());
+ dest.writeByteArray(d.getAddress());
+ }
+ if (mGateway != null) {
+ dest.writeByte((byte)1);
+ dest.writeString(mGateway.getHostName());
+ dest.writeByteArray(mGateway.getAddress());
+ } else {
+ dest.writeByte((byte)0);
+ }
+ if (mHttpProxy != null) {
+ dest.writeByte((byte)1);
+ dest.writeParcelable(mHttpProxy, flags);
+ } else {
+ dest.writeByte((byte)0);
+ }
+ }
+
+ /**
+ * Implement the Parcelable interface.
+ * @hide
+ */
+ public static final Creator<NetworkProperties> CREATOR =
+ new Creator<NetworkProperties>() {
+ public NetworkProperties createFromParcel(Parcel in) {
+ NetworkProperties netProp = new NetworkProperties();
+ String iface = in.readString();
+ if (iface != null) {
+ try {
+ netProp.setInterface(NetworkInterface.getByName(iface));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ int addressCount = in.readInt();
+ for (int i=0; i<addressCount; i++) {
+ try {
+ netProp.addAddress(InetAddress.getByAddress(in.readString(),
+ in.createByteArray()));
+ } catch (UnknownHostException e) { }
+ }
+ addressCount = in.readInt();
+ for (int i=0; i<addressCount; i++) {
+ try {
+ netProp.addDns(InetAddress.getByAddress(in.readString(),
+ in.createByteArray()));
+ } catch (UnknownHostException e) { }
+ }
+ if (in.readByte() == 1) {
+ try {
+ netProp.setGateway(InetAddress.getByAddress(in.readString(),
+ in.createByteArray()));
+ } catch (UnknownHostException e) {}
+ }
+ if (in.readByte() == 1) {
+ netProp.setHttpProxy((ProxyProperties)in.readParcelable(null));
+ }
+ return netProp;
+ }
+
+ public NetworkProperties[] newArray(int size) {
+ return new NetworkProperties[size];
+ }
+ };
+}
diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java
index 1fb0144..44215e7 100644
--- a/core/java/android/net/NetworkStateTracker.java
+++ b/core/java/android/net/NetworkStateTracker.java
@@ -16,40 +16,15 @@
package android.net;
-import java.io.FileWriter;
-import java.io.IOException;
-
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemProperties;
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Config;
-import android.util.Log;
-
-
/**
- * Each subclass of this class keeps track of the state of connectivity
- * of a network interface. All state information for a network should
- * be kept in a Tracker class. This superclass manages the
- * network-type-independent aspects of network state.
+ * Interface for connectivity service to act on a network interface.
+ * All state information for a network should be kept in a Tracker class.
+ * This interface defines network-type-independent functions that should
+ * be implemented by the Tracker class.
*
* {@hide}
*/
-public abstract class NetworkStateTracker extends Handler {
-
- protected NetworkInfo mNetworkInfo;
- protected Context mContext;
- protected Handler mTarget;
- protected String mInterfaceName;
- protected String[] mDnsPropNames;
- private boolean mPrivateDnsRouteSet;
- protected int mDefaultGatewayAddr;
- private boolean mDefaultRouteSet;
- private boolean mTeardownRequested;
-
- private static boolean DBG = true;
- private static final String TAG = "NetworkStateTracker";
+public interface NetworkStateTracker {
public static final int EVENT_STATE_CHANGED = 1;
public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2;
@@ -63,306 +38,86 @@ public abstract class NetworkStateTracker extends Handler {
public static final int EVENT_ROAMING_CHANGED = 5;
public static final int EVENT_NETWORK_SUBTYPE_CHANGED = 6;
public static final int EVENT_RESTORE_DEFAULT_NETWORK = 7;
-
- public NetworkStateTracker(Context context,
- Handler target,
- int networkType,
- int subType,
- String typeName,
- String subtypeName) {
- super();
- mContext = context;
- mTarget = target;
- mTeardownRequested = false;
-
- this.mNetworkInfo = new NetworkInfo(networkType, subType, typeName, subtypeName);
- }
-
- public NetworkInfo getNetworkInfo() {
- return mNetworkInfo;
- }
+ public static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8;
/**
- * Return the system properties name associated with the tcp buffer sizes
- * for this network.
+ * Fetch NetworkInfo for the network
*/
- public abstract String getTcpBufferSizesPropName();
+ public NetworkInfo getNetworkInfo();
/**
- * Return the IP addresses of the DNS servers available for the mobile data
- * network interface.
- * @return a list of DNS addresses, with no holes.
+ * Fetch NetworkProperties for the network
*/
- public String[] getNameServers() {
- return getNameServerList(mDnsPropNames);
- }
-
- /**
- * Return the IP addresses of the DNS servers available for this
- * network interface.
- * @param propertyNames the names of the system properties whose values
- * give the IP addresses. Properties with no values are skipped.
- * @return an array of {@code String}s containing the IP addresses
- * of the DNS servers, in dot-notation. This may have fewer
- * non-null entries than the list of names passed in, since
- * some of the passed-in names may have empty values.
- */
- static protected String[] getNameServerList(String[] propertyNames) {
- String[] dnsAddresses = new String[propertyNames.length];
- int i, j;
-
- for (i = 0, j = 0; i < propertyNames.length; i++) {
- String value = SystemProperties.get(propertyNames[i]);
- // The GSM layer sometimes sets a bogus DNS server address of
- // 0.0.0.0
- if (!TextUtils.isEmpty(value) && !TextUtils.equals(value, "0.0.0.0")) {
- dnsAddresses[j++] = value;
- }
- }
- return dnsAddresses;
- }
-
- public void addPrivateDnsRoutes() {
- if (DBG) {
- Log.d(TAG, "addPrivateDnsRoutes for " + this +
- "(" + mInterfaceName + ") - mPrivateDnsRouteSet = "+mPrivateDnsRouteSet);
- }
- if (mInterfaceName != null && !mPrivateDnsRouteSet) {
- for (String addrString : getNameServers()) {
- int addr = NetworkUtils.lookupHost(addrString);
- if (addr != -1 && addr != 0) {
- if (DBG) Log.d(TAG, " adding "+addrString+" ("+addr+")");
- NetworkUtils.addHostRoute(mInterfaceName, addr);
- }
- }
- mPrivateDnsRouteSet = true;
- }
- }
-
- public void removePrivateDnsRoutes() {
- // TODO - we should do this explicitly but the NetUtils api doesnt
- // support this yet - must remove all. No worse than before
- if (mInterfaceName != null && mPrivateDnsRouteSet) {
- if (DBG) {
- Log.d(TAG, "removePrivateDnsRoutes for " + mNetworkInfo.getTypeName() +
- " (" + mInterfaceName + ")");
- }
- NetworkUtils.removeHostRoutes(mInterfaceName);
- mPrivateDnsRouteSet = false;
- }
- }
-
- public void addDefaultRoute() {
- if ((mInterfaceName != null) && (mDefaultGatewayAddr != 0) &&
- mDefaultRouteSet == false) {
- if (DBG) {
- Log.d(TAG, "addDefaultRoute for " + mNetworkInfo.getTypeName() +
- " (" + mInterfaceName + "), GatewayAddr=" + mDefaultGatewayAddr);
- }
- NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr);
- mDefaultRouteSet = true;
- }
- }
-
- public void removeDefaultRoute() {
- if (mInterfaceName != null && mDefaultRouteSet == true) {
- if (DBG) {
- Log.d(TAG, "removeDefaultRoute for " + mNetworkInfo.getTypeName() + " (" +
- mInterfaceName + ")");
- }
- NetworkUtils.removeDefaultRoute(mInterfaceName);
- mDefaultRouteSet = false;
- }
- }
+ public NetworkProperties getNetworkProperties();
/**
- * Reads the network specific TCP buffer sizes from SystemProperties
- * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system
- * wide use
+ * Return the system properties name associated with the tcp buffer sizes
+ * for this network.
*/
- public void updateNetworkSettings() {
- String key = getTcpBufferSizesPropName();
- String bufferSizes = SystemProperties.get(key);
-
- if (bufferSizes.length() == 0) {
- Log.e(TAG, key + " not found in system properties. Using defaults");
-
- // Setting to default values so we won't be stuck to previous values
- key = "net.tcp.buffersize.default";
- bufferSizes = SystemProperties.get(key);
- }
-
- // Set values in kernel
- if (bufferSizes.length() != 0) {
- if (DBG) {
- Log.v(TAG, "Setting TCP values: [" + bufferSizes
- + "] which comes from [" + key + "]");
- }
- setBufferSize(bufferSizes);
- }
- }
+ public String getTcpBufferSizesPropName();
/**
- * Release the wakelock, if any, that may be held while handling a
- * disconnect operation.
+ * Check if private DNS route is set for the network
*/
- public void releaseWakeLock() {
- }
+ public boolean isPrivateDnsRouteSet();
/**
- * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max]
- * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem
- *
- * @param bufferSizes in the format of "readMin, readInitial, readMax,
- * writeMin, writeInitial, writeMax"
+ * Set a flag indicating private DNS route is set
*/
- private void setBufferSize(String bufferSizes) {
- try {
- String[] values = bufferSizes.split(",");
-
- if (values.length == 6) {
- final String prefix = "/sys/kernel/ipv4/tcp_";
- stringToFile(prefix + "rmem_min", values[0]);
- stringToFile(prefix + "rmem_def", values[1]);
- stringToFile(prefix + "rmem_max", values[2]);
- stringToFile(prefix + "wmem_min", values[3]);
- stringToFile(prefix + "wmem_def", values[4]);
- stringToFile(prefix + "wmem_max", values[5]);
- } else {
- Log.e(TAG, "Invalid buffersize string: " + bufferSizes);
- }
- } catch (IOException e) {
- Log.e(TAG, "Can't set tcp buffer sizes:" + e);
- }
- }
+ public void privateDnsRouteSet(boolean enabled);
/**
- * Writes string to file. Basically same as "echo -n $string > $filename"
- *
- * @param filename
- * @param string
- * @throws IOException
+ * Fetch default gateway address for the network
*/
- private void stringToFile(String filename, String string) throws IOException {
- FileWriter out = new FileWriter(filename);
- try {
- out.write(string);
- } finally {
- out.close();
- }
- }
+ public int getDefaultGatewayAddr();
/**
- * Record the detailed state of a network, and if it is a
- * change from the previous state, send a notification to
- * any listeners.
- * @param state the new @{code DetailedState}
+ * Check if default route is set
*/
- public void setDetailedState(NetworkInfo.DetailedState state) {
- setDetailedState(state, null, null);
- }
+ public boolean isDefaultRouteSet();
/**
- * Record the detailed state of a network, and if it is a
- * change from the previous state, send a notification to
- * any listeners.
- * @param state the new @{code DetailedState}
- * @param reason a {@code String} indicating a reason for the state change,
- * if one was supplied. May be {@code null}.
- * @param extraInfo optional {@code String} providing extra information about the state change
+ * Set a flag indicating default route is set for the network
*/
- public void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) {
- if (DBG) Log.d(TAG, "setDetailed state, old ="+mNetworkInfo.getDetailedState()+" and new state="+state);
- if (state != mNetworkInfo.getDetailedState()) {
- boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING);
- String lastReason = mNetworkInfo.getReason();
- /*
- * If a reason was supplied when the CONNECTING state was entered, and no
- * reason was supplied for entering the CONNECTED state, then retain the
- * reason that was supplied when going to CONNECTING.
- */
- if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null
- && lastReason != null)
- reason = lastReason;
- mNetworkInfo.setDetailedState(state, reason, extraInfo);
- Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo);
- msg.sendToTarget();
- }
- }
-
- protected void setDetailedStateInternal(NetworkInfo.DetailedState state) {
- mNetworkInfo.setDetailedState(state, null, null);
- }
+ public void defaultRouteSet(boolean enabled);
- public void setTeardownRequested(boolean isRequested) {
- mTeardownRequested = isRequested;
- }
-
- public boolean isTeardownRequested() {
- return mTeardownRequested;
- }
-
/**
- * Send a notification that the results of a scan for network access
- * points has completed, and results are available.
+ * Indicate tear down requested from connectivity
*/
- protected void sendScanResultsAvailable() {
- Message msg = mTarget.obtainMessage(EVENT_SCAN_RESULTS_AVAILABLE, mNetworkInfo);
- msg.sendToTarget();
- }
+ public void setTeardownRequested(boolean isRequested);
/**
- * Record the roaming status of the device, and if it is a change from the previous
- * status, send a notification to any listeners.
- * @param isRoaming {@code true} if the device is now roaming, {@code false}
- * if it is no longer roaming.
+ * Check if tear down was requested
*/
- protected void setRoamingStatus(boolean isRoaming) {
- if (isRoaming != mNetworkInfo.isRoaming()) {
- mNetworkInfo.setRoaming(isRoaming);
- Message msg = mTarget.obtainMessage(EVENT_ROAMING_CHANGED, mNetworkInfo);
- msg.sendToTarget();
- }
- }
-
- protected void setSubtype(int subtype, String subtypeName) {
- if (mNetworkInfo.isConnected()) {
- int oldSubtype = mNetworkInfo.getSubtype();
- if (subtype != oldSubtype) {
- mNetworkInfo.setSubtype(subtype, subtypeName);
- Message msg = mTarget.obtainMessage(
- EVENT_NETWORK_SUBTYPE_CHANGED, oldSubtype, 0, mNetworkInfo);
- msg.sendToTarget();
- }
- }
- }
+ public boolean isTeardownRequested();
- public abstract void startMonitoring();
+ public void startMonitoring();
/**
* Disable connectivity to a network
* @return {@code true} if a teardown occurred, {@code false} if the
* teardown did not occur.
*/
- public abstract boolean teardown();
+ public boolean teardown();
/**
* Reenable connectivity to a network after a {@link #teardown()}.
+ * @return {@code true} if we're connected or expect to be connected
*/
- public abstract boolean reconnect();
+ public boolean reconnect();
/**
* Turn the wireless radio off for a network.
* @param turnOn {@code true} to turn the radio on, {@code false}
*/
- public abstract boolean setRadio(boolean turnOn);
+ public boolean setRadio(boolean turnOn);
/**
* Returns an indication of whether this network is available for
* connections. A value of {@code false} means that some quasi-permanent
* condition prevents connectivity to this network.
*/
- public abstract boolean isAvailable();
+ public boolean isAvailable();
/**
* Tells the underlying networking system that the caller wants to
@@ -376,7 +131,7 @@ public abstract class NetworkStateTracker extends Handler {
* implementation+feature combination, except that the value {@code -1}
* always indicates failure.
*/
- public abstract int startUsingNetworkFeature(String feature, int callingPid, int callingUid);
+ public int startUsingNetworkFeature(String feature, int callingPid, int callingUid);
/**
* Tells the underlying networking system that the caller is finished
@@ -390,23 +145,12 @@ public abstract class NetworkStateTracker extends Handler {
* implementation+feature combination, except that the value {@code -1}
* always indicates failure.
*/
- public abstract int stopUsingNetworkFeature(String feature, int callingPid, int callingUid);
-
- /**
- * Ensure that a network route exists to deliver traffic to the specified
- * host via this network interface.
- * @param hostAddress the IP address of the host to which the route is desired
- * @return {@code true} on success, {@code false} on failure
- */
- public boolean requestRouteToHost(int hostAddress) {
- return false;
- }
+ public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid);
/**
* Interprets scan results. This will be called at a safe time for
* processing, and from a safe thread.
*/
- public void interpretScanResultsAvailable() {
- }
+ public void interpretScanResultsAvailable();
}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index a3ae01b..564bc1f 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -32,13 +32,37 @@ public class NetworkUtils {
public native static int disableInterface(String interfaceName);
/** Add a route to the specified host via the named interface. */
- public native static int addHostRoute(String interfaceName, int hostaddr);
+ public static int addHostRoute(String interfaceName, InetAddress hostaddr) {
+ int v4Int = v4StringToInt(hostaddr.getHostAddress());
+ if (v4Int != 0) {
+ return addHostRouteNative(interfaceName, v4Int);
+ } else {
+ return -1;
+ }
+ }
+ private native static int addHostRouteNative(String interfaceName, int hostaddr);
/** Add a default route for the named interface. */
- public native static int setDefaultRoute(String interfaceName, int gwayAddr);
+ public static int setDefaultRoute(String interfaceName, InetAddress gwayAddr) {
+ int v4Int = v4StringToInt(gwayAddr.getHostAddress());
+ if (v4Int != 0) {
+ return setDefaultRouteNative(interfaceName, v4Int);
+ } else {
+ return -1;
+ }
+ }
+ private native static int setDefaultRouteNative(String interfaceName, int hostaddr);
/** Return the gateway address for the default route for the named interface. */
- public native static int getDefaultRoute(String interfaceName);
+ public static InetAddress getDefaultRoute(String interfaceName) {
+ int addr = getDefaultRouteNative(interfaceName);
+ try {
+ return InetAddress.getByAddress(v4IntToArray(addr));
+ } catch (UnknownHostException e) {
+ return null;
+ }
+ }
+ private native static int getDefaultRouteNative(String interfaceName);
/** Remove host routes that uses the named interface. */
public native static int removeHostRoutes(String interfaceName);
@@ -105,27 +129,30 @@ public class NetworkUtils {
private native static boolean configureNative(
String interfaceName, int ipAddress, int netmask, int gateway, int dns1, int dns2);
- /**
- * Look up a host name and return the result as an int. Works if the argument
- * is an IP address in dot notation. Obviously, this can only be used for IPv4
- * addresses.
- * @param hostname the name of the host (or the IP address)
- * @return the IP address as an {@code int} in network byte order
- */
- public static int lookupHost(String hostname) {
- InetAddress inetAddress;
+ // The following two functions are glue to tie the old int-based address scheme
+ // to the new InetAddress scheme. They should go away when we go fully to InetAddress
+ // TODO - remove when we switch fully to InetAddress
+ public static byte[] v4IntToArray(int addr) {
+ byte[] addrBytes = new byte[4];
+ addrBytes[0] = (byte)(addr & 0xff);
+ addrBytes[1] = (byte)((addr >> 8) & 0xff);
+ addrBytes[2] = (byte)((addr >> 16) & 0xff);
+ addrBytes[3] = (byte)((addr >> 24) & 0xff);
+ return addrBytes;
+ }
+
+ public static int v4StringToInt(String str) {
+ int result = 0;
+ String[] array = str.split("\\.");
+ if (array.length != 4) return 0;
try {
- inetAddress = InetAddress.getByName(hostname);
- } catch (UnknownHostException e) {
- return -1;
+ result = Integer.parseInt(array[3]);
+ result = (result << 8) + Integer.parseInt(array[2]);
+ result = (result << 8) + Integer.parseInt(array[1]);
+ result = (result << 8) + Integer.parseInt(array[0]);
+ } catch (NumberFormatException e) {
+ return 0;
}
- byte[] addrBytes;
- int addr;
- addrBytes = inetAddress.getAddress();
- addr = ((addrBytes[3] & 0xff) << 24)
- | ((addrBytes[2] & 0xff) << 16)
- | ((addrBytes[1] & 0xff) << 8)
- | (addrBytes[0] & 0xff);
- return addr;
+ return result;
}
}
diff --git a/core/java/android/net/ProxyProperties.java b/core/java/android/net/ProxyProperties.java
new file mode 100644
index 0000000..6828dd4
--- /dev/null
+++ b/core/java/android/net/ProxyProperties.java
@@ -0,0 +1,108 @@
+/*
+ * 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.net;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * A container class for the http proxy info
+ * @hide
+ */
+public class ProxyProperties implements Parcelable {
+
+ private InetAddress mProxy;
+ private int mPort;
+ private String mExclusionList;
+
+ public ProxyProperties() {
+ }
+
+ public synchronized InetAddress getAddress() {
+ return mProxy;
+ }
+ public synchronized void setAddress(InetAddress proxy) {
+ mProxy = proxy;
+ }
+
+ public synchronized int getPort() {
+ return mPort;
+ }
+ public synchronized void setPort(int port) {
+ mPort = port;
+ }
+
+ public synchronized String getExclusionList() {
+ return mExclusionList;
+ }
+ public synchronized void setExclusionList(String exclusionList) {
+ mExclusionList = exclusionList;
+ }
+
+ /**
+ * Implement the Parcelable interface
+ * @hide
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Implement the Parcelable interface.
+ * @hide
+ */
+ public synchronized void writeToParcel(Parcel dest, int flags) {
+ if (mProxy != null) {
+ dest.writeByte((byte)1);
+ dest.writeString(mProxy.getHostName());
+ dest.writeByteArray(mProxy.getAddress());
+ } else {
+ dest.writeByte((byte)0);
+ }
+ dest.writeInt(mPort);
+ dest.writeString(mExclusionList);
+ }
+
+ /**
+ * Implement the Parcelable interface.
+ * @hide
+ */
+ public static final Creator<ProxyProperties> CREATOR =
+ new Creator<ProxyProperties>() {
+ public ProxyProperties createFromParcel(Parcel in) {
+ ProxyProperties proxyProperties = new ProxyProperties();
+ if (in.readByte() == 1) {
+ try {
+ proxyProperties.setAddress(InetAddress.getByAddress(in.readString(),
+ in.createByteArray()));
+ } catch (UnknownHostException e) {}
+ }
+ proxyProperties.setPort(in.readInt());
+ proxyProperties.setExclusionList(in.readString());
+ return proxyProperties;
+ }
+
+ public ProxyProperties[] newArray(int size) {
+ return new ProxyProperties[size];
+ }
+ };
+
+};
diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java
index e07ee59..915e342 100644
--- a/core/java/android/net/http/AndroidHttpClient.java
+++ b/core/java/android/net/http/AndroidHttpClient.java
@@ -61,6 +61,7 @@ import android.content.ContentResolver;
import android.net.SSLCertificateSocketFactory;
import android.net.SSLSessionCache;
import android.os.Looper;
+import android.util.Base64;
import android.util.Log;
/**
@@ -81,6 +82,11 @@ public final class AndroidHttpClient implements HttpClient {
private static final String TAG = "AndroidHttpClient";
+ private static String[] textContentTypes = new String[] {
+ "text/",
+ "application/xml",
+ "application/json"
+ };
/** Interceptor throws an exception if the executing thread is blocked */
private static final HttpRequestInterceptor sThreadCheckInterceptor =
@@ -358,7 +364,7 @@ public final class AndroidHttpClient implements HttpClient {
}
if (level < Log.VERBOSE || level > Log.ASSERT) {
throw new IllegalArgumentException("Level is out of range ["
- + Log.VERBOSE + ".." + Log.ASSERT + "]");
+ + Log.VERBOSE + ".." + Log.ASSERT + "]");
}
curlConfiguration = new LoggingConfiguration(name, level);
@@ -431,12 +437,17 @@ public final class AndroidHttpClient implements HttpClient {
if (entity.getContentLength() < 1024) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
entity.writeTo(stream);
- String entityString = stream.toString();
- // TODO: Check the content type, too.
- builder.append(" --data-ascii \"")
- .append(entityString)
- .append("\"");
+ if (isBinaryContent(request)) {
+ String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP);
+ builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; ");
+ builder.append(" --data-binary @/tmp/$$.bin");
+ } else {
+ String entityString = stream.toString();
+ builder.append(" --data-ascii \"")
+ .append(entityString)
+ .append("\"");
+ }
} else {
builder.append(" [TOO MUCH DATA TO INCLUDE]");
}
@@ -446,6 +457,30 @@ public final class AndroidHttpClient implements HttpClient {
return builder.toString();
}
+ private static boolean isBinaryContent(HttpUriRequest request) {
+ Header[] headers;
+ headers = request.getHeaders(Headers.CONTENT_ENCODING);
+ if (headers != null) {
+ for (Header header : headers) {
+ if ("gzip".equalsIgnoreCase(header.getValue())) {
+ return true;
+ }
+ }
+ }
+
+ headers = request.getHeaders(Headers.CONTENT_TYPE);
+ if (headers != null) {
+ for (Header header : headers) {
+ for (String contentType : textContentTypes) {
+ if (header.getValue().startsWith(contentType)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
/**
* Returns the date of the given HTTP date string. This method can identify
* and parse the date formats emitted by common HTTP servers, such as
diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java
index c527fe4..c36ad38 100644
--- a/core/java/android/net/http/CertificateChainValidator.java
+++ b/core/java/android/net/http/CertificateChainValidator.java
@@ -80,14 +80,10 @@ class CertificateChainValidator {
throws IOException {
X509Certificate[] serverCertificates = null;
- // start handshake, close the socket if we fail
- try {
- sslSocket.setUseClientMode(true);
- sslSocket.startHandshake();
- } catch (IOException e) {
- closeSocketThrowException(
- sslSocket, e.getMessage(),
- "failed to perform SSL handshake");
+ // get a valid SSLSession, close the socket if we fail
+ SSLSession sslSession = sslSession = sslSocket.getSession();
+ if (!sslSession.isValid()) {
+ closeSocketThrowException(sslSocket, "failed to perform SSL handshake");
}
// retrieve the chain of the server peer certificates
diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java
index d28148c..832ce84 100644
--- a/core/java/android/os/AsyncTask.java
+++ b/core/java/android/os/AsyncTask.java
@@ -16,16 +16,16 @@
package android.os;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.Callable;
-import java.util.concurrent.FutureTask;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -36,8 +36,8 @@ import java.util.concurrent.atomic.AtomicInteger;
* <p>An asynchronous task is defined by a computation that runs on a background thread and
* whose result is published on the UI thread. An asynchronous task is defined by 3 generic
* types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
- * and 4 steps, called <code>begin</code>, <code>doInBackground</code>,
- * <code>processProgress</code> and <code>end</code>.</p>
+ * and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>,
+ * <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p>
*
* <h2>Usage</h2>
* <p>AsyncTask must be subclassed to be used. The subclass will override at least
@@ -187,6 +187,17 @@ public abstract class AsyncTask<Params, Progress, Result> {
};
mFuture = new FutureTask<Result>(mWorker) {
+
+ @Override
+ protected void set(Result v) {
+ super.set(v);
+ if (isCancelled()) {
+ Message message = sHandler.obtainMessage(MESSAGE_POST_CANCEL,
+ new AsyncTaskResult<Result>(AsyncTask.this, (Result[]) null));
+ message.sendToTarget();
+ }
+ }
+
@Override
protected void done() {
Message message;
@@ -402,14 +413,19 @@ public abstract class AsyncTask<Params, Progress, Result> {
* still running. Each call to this method will trigger the execution of
* {@link #onProgressUpdate} on the UI thread.
*
+ * {@link #onProgressUpdate} will note be called if the task has been
+ * canceled.
+ *
* @param values The progress values to update the UI with.
*
* @see #onProgressUpdate
* @see #doInBackground
*/
protected final void publishProgress(Progress... values) {
- sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
- new AsyncTaskResult<Progress>(this, values)).sendToTarget();
+ if (!isCancelled()) {
+ sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
+ new AsyncTaskResult<Progress>(this, values)).sendToTarget();
+ }
}
private void finish(Result result) {
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index 2e14667..d23b161 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -730,7 +730,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo
}
/**
- * Dump "hprof" data to the specified file. This will cause a GC.
+ * Dump "hprof" data to the specified file. This may cause a GC.
*
* @param fileName Full pathname of output file (e.g. "/sdcard/dump.hprof").
* @throws UnsupportedOperationException if the VM was built without
@@ -742,11 +742,24 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo
}
/**
- * Collect "hprof" and send it to DDMS. This will cause a GC.
+ * Like dumpHprofData(String), but takes an already-opened
+ * FileDescriptor to which the trace is written. The file name is also
+ * supplied simply for logging. Makes a dup of the file descriptor.
+ *
+ * Primarily for use by the "am" shell command.
+ *
+ * @hide
+ */
+ public static void dumpHprofData(String fileName, FileDescriptor fd)
+ throws IOException {
+ VMDebug.dumpHprofData(fileName, fd);
+ }
+
+ /**
+ * Collect "hprof" and send it to DDMS. This may cause a GC.
*
* @throws UnsupportedOperationException if the VM was built without
* HPROF support.
- *
* @hide
*/
public static void dumpHprofDataDdms() {
@@ -754,6 +767,13 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo
}
/**
+ * Writes native heap data to the specified file descriptor.
+ *
+ * @hide
+ */
+ public static native void dumpNativeHeap(FileDescriptor fd);
+
+ /**
* Returns the number of sent transactions from this process.
* @return The number of sent transactions or -1 if it could not read t.
*/
diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java
index 7b883a7..d3d39d6 100644
--- a/core/java/android/os/storage/StorageEventListener.java
+++ b/core/java/android/os/storage/StorageEventListener.java
@@ -18,7 +18,6 @@ package android.os.storage;
/**
* Used for receiving notifications from the StorageManager
- * @hide
*/
public abstract class StorageEventListener {
/**
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index a12603c..b49979c 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -45,8 +45,6 @@ import java.util.List;
* {@link android.content.Context#getSystemService(java.lang.String)} with an argument
* of {@link android.content.Context#STORAGE_SERVICE}.
*
- * @hide
- *
*/
public class StorageManager
diff --git a/core/java/android/os/storage/StorageResultCode.java b/core/java/android/os/storage/StorageResultCode.java
index 075f47f..07d95df 100644
--- a/core/java/android/os/storage/StorageResultCode.java
+++ b/core/java/android/os/storage/StorageResultCode.java
@@ -19,8 +19,6 @@ package android.os.storage;
/**
* Class that provides access to constants returned from StorageManager
* and lower level MountService APIs.
- *
- * @hide
*/
public class StorageResultCode
{
diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java
index 635323e..282417d 100644
--- a/core/java/android/pim/RecurrenceSet.java
+++ b/core/java/android/pim/RecurrenceSet.java
@@ -181,7 +181,9 @@ public class RecurrenceSet {
boolean inUtc = start.parse(dtstart);
boolean allDay = start.allDay;
- if (inUtc) {
+ // We force TimeZone to UTC for "all day recurring events" as the server is sending no
+ // TimeZone in DTSTART for them
+ if (inUtc || allDay) {
tzid = Time.TIMEZONE_UTC;
}
@@ -204,10 +206,7 @@ public class RecurrenceSet {
}
if (allDay) {
- // TODO: also change tzid to be UTC? that would be consistent, but
- // that would not reflect the original timezone value back to the
- // server.
- start.timezone = Time.TIMEZONE_UTC;
+ start.timezone = Time.TIMEZONE_UTC;
}
long millis = start.toMillis(false /* use isDst */);
values.put(Calendar.Events.DTSTART, millis);
diff --git a/core/java/android/pim/vcard/JapaneseUtils.java b/core/java/android/pim/vcard/JapaneseUtils.java
deleted file mode 100644
index 875c29e..0000000
--- a/core/java/android/pim/vcard/JapaneseUtils.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.pim.vcard;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * TextUtils especially for Japanese.
- */
-/* package */ class JapaneseUtils {
- static private final Map<Character, String> sHalfWidthMap =
- new HashMap<Character, String>();
-
- static {
- // There's no logical mapping rule in Unicode. Sigh.
- sHalfWidthMap.put('\u3001', "\uFF64");
- sHalfWidthMap.put('\u3002', "\uFF61");
- sHalfWidthMap.put('\u300C', "\uFF62");
- sHalfWidthMap.put('\u300D', "\uFF63");
- sHalfWidthMap.put('\u301C', "~");
- sHalfWidthMap.put('\u3041', "\uFF67");
- sHalfWidthMap.put('\u3042', "\uFF71");
- sHalfWidthMap.put('\u3043', "\uFF68");
- sHalfWidthMap.put('\u3044', "\uFF72");
- sHalfWidthMap.put('\u3045', "\uFF69");
- sHalfWidthMap.put('\u3046', "\uFF73");
- sHalfWidthMap.put('\u3047', "\uFF6A");
- sHalfWidthMap.put('\u3048', "\uFF74");
- sHalfWidthMap.put('\u3049', "\uFF6B");
- sHalfWidthMap.put('\u304A', "\uFF75");
- sHalfWidthMap.put('\u304B', "\uFF76");
- sHalfWidthMap.put('\u304C', "\uFF76\uFF9E");
- sHalfWidthMap.put('\u304D', "\uFF77");
- sHalfWidthMap.put('\u304E', "\uFF77\uFF9E");
- sHalfWidthMap.put('\u304F', "\uFF78");
- sHalfWidthMap.put('\u3050', "\uFF78\uFF9E");
- sHalfWidthMap.put('\u3051', "\uFF79");
- sHalfWidthMap.put('\u3052', "\uFF79\uFF9E");
- sHalfWidthMap.put('\u3053', "\uFF7A");
- sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E");
- sHalfWidthMap.put('\u3055', "\uFF7B");
- sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E");
- sHalfWidthMap.put('\u3057', "\uFF7C");
- sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E");
- sHalfWidthMap.put('\u3059', "\uFF7D");
- sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E");
- sHalfWidthMap.put('\u305B', "\uFF7E");
- sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E");
- sHalfWidthMap.put('\u305D', "\uFF7F");
- sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E");
- sHalfWidthMap.put('\u305F', "\uFF80");
- sHalfWidthMap.put('\u3060', "\uFF80\uFF9E");
- sHalfWidthMap.put('\u3061', "\uFF81");
- sHalfWidthMap.put('\u3062', "\uFF81\uFF9E");
- sHalfWidthMap.put('\u3063', "\uFF6F");
- sHalfWidthMap.put('\u3064', "\uFF82");
- sHalfWidthMap.put('\u3065', "\uFF82\uFF9E");
- sHalfWidthMap.put('\u3066', "\uFF83");
- sHalfWidthMap.put('\u3067', "\uFF83\uFF9E");
- sHalfWidthMap.put('\u3068', "\uFF84");
- sHalfWidthMap.put('\u3069', "\uFF84\uFF9E");
- sHalfWidthMap.put('\u306A', "\uFF85");
- sHalfWidthMap.put('\u306B', "\uFF86");
- sHalfWidthMap.put('\u306C', "\uFF87");
- sHalfWidthMap.put('\u306D', "\uFF88");
- sHalfWidthMap.put('\u306E', "\uFF89");
- sHalfWidthMap.put('\u306F', "\uFF8A");
- sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E");
- sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F");
- sHalfWidthMap.put('\u3072', "\uFF8B");
- sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E");
- sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F");
- sHalfWidthMap.put('\u3075', "\uFF8C");
- sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E");
- sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F");
- sHalfWidthMap.put('\u3078', "\uFF8D");
- sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E");
- sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F");
- sHalfWidthMap.put('\u307B', "\uFF8E");
- sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E");
- sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F");
- sHalfWidthMap.put('\u307E', "\uFF8F");
- sHalfWidthMap.put('\u307F', "\uFF90");
- sHalfWidthMap.put('\u3080', "\uFF91");
- sHalfWidthMap.put('\u3081', "\uFF92");
- sHalfWidthMap.put('\u3082', "\uFF93");
- sHalfWidthMap.put('\u3083', "\uFF6C");
- sHalfWidthMap.put('\u3084', "\uFF94");
- sHalfWidthMap.put('\u3085', "\uFF6D");
- sHalfWidthMap.put('\u3086', "\uFF95");
- sHalfWidthMap.put('\u3087', "\uFF6E");
- sHalfWidthMap.put('\u3088', "\uFF96");
- sHalfWidthMap.put('\u3089', "\uFF97");
- sHalfWidthMap.put('\u308A', "\uFF98");
- sHalfWidthMap.put('\u308B', "\uFF99");
- sHalfWidthMap.put('\u308C', "\uFF9A");
- sHalfWidthMap.put('\u308D', "\uFF9B");
- sHalfWidthMap.put('\u308E', "\uFF9C");
- sHalfWidthMap.put('\u308F', "\uFF9C");
- sHalfWidthMap.put('\u3090', "\uFF72");
- sHalfWidthMap.put('\u3091', "\uFF74");
- sHalfWidthMap.put('\u3092', "\uFF66");
- sHalfWidthMap.put('\u3093', "\uFF9D");
- sHalfWidthMap.put('\u309B', "\uFF9E");
- sHalfWidthMap.put('\u309C', "\uFF9F");
- sHalfWidthMap.put('\u30A1', "\uFF67");
- sHalfWidthMap.put('\u30A2', "\uFF71");
- sHalfWidthMap.put('\u30A3', "\uFF68");
- sHalfWidthMap.put('\u30A4', "\uFF72");
- sHalfWidthMap.put('\u30A5', "\uFF69");
- sHalfWidthMap.put('\u30A6', "\uFF73");
- sHalfWidthMap.put('\u30A7', "\uFF6A");
- sHalfWidthMap.put('\u30A8', "\uFF74");
- sHalfWidthMap.put('\u30A9', "\uFF6B");
- sHalfWidthMap.put('\u30AA', "\uFF75");
- sHalfWidthMap.put('\u30AB', "\uFF76");
- sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E");
- sHalfWidthMap.put('\u30AD', "\uFF77");
- sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E");
- sHalfWidthMap.put('\u30AF', "\uFF78");
- sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E");
- sHalfWidthMap.put('\u30B1', "\uFF79");
- sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E");
- sHalfWidthMap.put('\u30B3', "\uFF7A");
- sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E");
- sHalfWidthMap.put('\u30B5', "\uFF7B");
- sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E");
- sHalfWidthMap.put('\u30B7', "\uFF7C");
- sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E");
- sHalfWidthMap.put('\u30B9', "\uFF7D");
- sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E");
- sHalfWidthMap.put('\u30BB', "\uFF7E");
- sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E");
- sHalfWidthMap.put('\u30BD', "\uFF7F");
- sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E");
- sHalfWidthMap.put('\u30BF', "\uFF80");
- sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E");
- sHalfWidthMap.put('\u30C1', "\uFF81");
- sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E");
- sHalfWidthMap.put('\u30C3', "\uFF6F");
- sHalfWidthMap.put('\u30C4', "\uFF82");
- sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E");
- sHalfWidthMap.put('\u30C6', "\uFF83");
- sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E");
- sHalfWidthMap.put('\u30C8', "\uFF84");
- sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E");
- sHalfWidthMap.put('\u30CA', "\uFF85");
- sHalfWidthMap.put('\u30CB', "\uFF86");
- sHalfWidthMap.put('\u30CC', "\uFF87");
- sHalfWidthMap.put('\u30CD', "\uFF88");
- sHalfWidthMap.put('\u30CE', "\uFF89");
- sHalfWidthMap.put('\u30CF', "\uFF8A");
- sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E");
- sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F");
- sHalfWidthMap.put('\u30D2', "\uFF8B");
- sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E");
- sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F");
- sHalfWidthMap.put('\u30D5', "\uFF8C");
- sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E");
- sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F");
- sHalfWidthMap.put('\u30D8', "\uFF8D");
- sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E");
- sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F");
- sHalfWidthMap.put('\u30DB', "\uFF8E");
- sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E");
- sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F");
- sHalfWidthMap.put('\u30DE', "\uFF8F");
- sHalfWidthMap.put('\u30DF', "\uFF90");
- sHalfWidthMap.put('\u30E0', "\uFF91");
- sHalfWidthMap.put('\u30E1', "\uFF92");
- sHalfWidthMap.put('\u30E2', "\uFF93");
- sHalfWidthMap.put('\u30E3', "\uFF6C");
- sHalfWidthMap.put('\u30E4', "\uFF94");
- sHalfWidthMap.put('\u30E5', "\uFF6D");
- sHalfWidthMap.put('\u30E6', "\uFF95");
- sHalfWidthMap.put('\u30E7', "\uFF6E");
- sHalfWidthMap.put('\u30E8', "\uFF96");
- sHalfWidthMap.put('\u30E9', "\uFF97");
- sHalfWidthMap.put('\u30EA', "\uFF98");
- sHalfWidthMap.put('\u30EB', "\uFF99");
- sHalfWidthMap.put('\u30EC', "\uFF9A");
- sHalfWidthMap.put('\u30ED', "\uFF9B");
- sHalfWidthMap.put('\u30EE', "\uFF9C");
- sHalfWidthMap.put('\u30EF', "\uFF9C");
- sHalfWidthMap.put('\u30F0', "\uFF72");
- sHalfWidthMap.put('\u30F1', "\uFF74");
- sHalfWidthMap.put('\u30F2', "\uFF66");
- sHalfWidthMap.put('\u30F3', "\uFF9D");
- sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E");
- sHalfWidthMap.put('\u30F5', "\uFF76");
- sHalfWidthMap.put('\u30F6', "\uFF79");
- sHalfWidthMap.put('\u30FB', "\uFF65");
- sHalfWidthMap.put('\u30FC', "\uFF70");
- sHalfWidthMap.put('\uFF01', "!");
- sHalfWidthMap.put('\uFF02', "\"");
- sHalfWidthMap.put('\uFF03', "#");
- sHalfWidthMap.put('\uFF04', "$");
- sHalfWidthMap.put('\uFF05', "%");
- sHalfWidthMap.put('\uFF06', "&");
- sHalfWidthMap.put('\uFF07', "'");
- sHalfWidthMap.put('\uFF08', "(");
- sHalfWidthMap.put('\uFF09', ")");
- sHalfWidthMap.put('\uFF0A', "*");
- sHalfWidthMap.put('\uFF0B', "+");
- sHalfWidthMap.put('\uFF0C', ",");
- sHalfWidthMap.put('\uFF0D', "-");
- sHalfWidthMap.put('\uFF0E', ".");
- sHalfWidthMap.put('\uFF0F', "/");
- sHalfWidthMap.put('\uFF10', "0");
- sHalfWidthMap.put('\uFF11', "1");
- sHalfWidthMap.put('\uFF12', "2");
- sHalfWidthMap.put('\uFF13', "3");
- sHalfWidthMap.put('\uFF14', "4");
- sHalfWidthMap.put('\uFF15', "5");
- sHalfWidthMap.put('\uFF16', "6");
- sHalfWidthMap.put('\uFF17', "7");
- sHalfWidthMap.put('\uFF18', "8");
- sHalfWidthMap.put('\uFF19', "9");
- sHalfWidthMap.put('\uFF1A', ":");
- sHalfWidthMap.put('\uFF1B', ";");
- sHalfWidthMap.put('\uFF1C', "<");
- sHalfWidthMap.put('\uFF1D', "=");
- sHalfWidthMap.put('\uFF1E', ">");
- sHalfWidthMap.put('\uFF1F', "?");
- sHalfWidthMap.put('\uFF20', "@");
- sHalfWidthMap.put('\uFF21', "A");
- sHalfWidthMap.put('\uFF22', "B");
- sHalfWidthMap.put('\uFF23', "C");
- sHalfWidthMap.put('\uFF24', "D");
- sHalfWidthMap.put('\uFF25', "E");
- sHalfWidthMap.put('\uFF26', "F");
- sHalfWidthMap.put('\uFF27', "G");
- sHalfWidthMap.put('\uFF28', "H");
- sHalfWidthMap.put('\uFF29', "I");
- sHalfWidthMap.put('\uFF2A', "J");
- sHalfWidthMap.put('\uFF2B', "K");
- sHalfWidthMap.put('\uFF2C', "L");
- sHalfWidthMap.put('\uFF2D', "M");
- sHalfWidthMap.put('\uFF2E', "N");
- sHalfWidthMap.put('\uFF2F', "O");
- sHalfWidthMap.put('\uFF30', "P");
- sHalfWidthMap.put('\uFF31', "Q");
- sHalfWidthMap.put('\uFF32', "R");
- sHalfWidthMap.put('\uFF33', "S");
- sHalfWidthMap.put('\uFF34', "T");
- sHalfWidthMap.put('\uFF35', "U");
- sHalfWidthMap.put('\uFF36', "V");
- sHalfWidthMap.put('\uFF37', "W");
- sHalfWidthMap.put('\uFF38', "X");
- sHalfWidthMap.put('\uFF39', "Y");
- sHalfWidthMap.put('\uFF3A', "Z");
- sHalfWidthMap.put('\uFF3B', "[");
- sHalfWidthMap.put('\uFF3C', "\\");
- sHalfWidthMap.put('\uFF3D', "]");
- sHalfWidthMap.put('\uFF3E', "^");
- sHalfWidthMap.put('\uFF3F', "_");
- sHalfWidthMap.put('\uFF41', "a");
- sHalfWidthMap.put('\uFF42', "b");
- sHalfWidthMap.put('\uFF43', "c");
- sHalfWidthMap.put('\uFF44', "d");
- sHalfWidthMap.put('\uFF45', "e");
- sHalfWidthMap.put('\uFF46', "f");
- sHalfWidthMap.put('\uFF47', "g");
- sHalfWidthMap.put('\uFF48', "h");
- sHalfWidthMap.put('\uFF49', "i");
- sHalfWidthMap.put('\uFF4A', "j");
- sHalfWidthMap.put('\uFF4B', "k");
- sHalfWidthMap.put('\uFF4C', "l");
- sHalfWidthMap.put('\uFF4D', "m");
- sHalfWidthMap.put('\uFF4E', "n");
- sHalfWidthMap.put('\uFF4F', "o");
- sHalfWidthMap.put('\uFF50', "p");
- sHalfWidthMap.put('\uFF51', "q");
- sHalfWidthMap.put('\uFF52', "r");
- sHalfWidthMap.put('\uFF53', "s");
- sHalfWidthMap.put('\uFF54', "t");
- sHalfWidthMap.put('\uFF55', "u");
- sHalfWidthMap.put('\uFF56', "v");
- sHalfWidthMap.put('\uFF57', "w");
- sHalfWidthMap.put('\uFF58', "x");
- sHalfWidthMap.put('\uFF59', "y");
- sHalfWidthMap.put('\uFF5A', "z");
- sHalfWidthMap.put('\uFF5B', "{");
- sHalfWidthMap.put('\uFF5C', "|");
- sHalfWidthMap.put('\uFF5D', "}");
- sHalfWidthMap.put('\uFF5E', "~");
- sHalfWidthMap.put('\uFF61', "\uFF61");
- sHalfWidthMap.put('\uFF62', "\uFF62");
- sHalfWidthMap.put('\uFF63', "\uFF63");
- sHalfWidthMap.put('\uFF64', "\uFF64");
- sHalfWidthMap.put('\uFF65', "\uFF65");
- sHalfWidthMap.put('\uFF66', "\uFF66");
- sHalfWidthMap.put('\uFF67', "\uFF67");
- sHalfWidthMap.put('\uFF68', "\uFF68");
- sHalfWidthMap.put('\uFF69', "\uFF69");
- sHalfWidthMap.put('\uFF6A', "\uFF6A");
- sHalfWidthMap.put('\uFF6B', "\uFF6B");
- sHalfWidthMap.put('\uFF6C', "\uFF6C");
- sHalfWidthMap.put('\uFF6D', "\uFF6D");
- sHalfWidthMap.put('\uFF6E', "\uFF6E");
- sHalfWidthMap.put('\uFF6F', "\uFF6F");
- sHalfWidthMap.put('\uFF70', "\uFF70");
- sHalfWidthMap.put('\uFF71', "\uFF71");
- sHalfWidthMap.put('\uFF72', "\uFF72");
- sHalfWidthMap.put('\uFF73', "\uFF73");
- sHalfWidthMap.put('\uFF74', "\uFF74");
- sHalfWidthMap.put('\uFF75', "\uFF75");
- sHalfWidthMap.put('\uFF76', "\uFF76");
- sHalfWidthMap.put('\uFF77', "\uFF77");
- sHalfWidthMap.put('\uFF78', "\uFF78");
- sHalfWidthMap.put('\uFF79', "\uFF79");
- sHalfWidthMap.put('\uFF7A', "\uFF7A");
- sHalfWidthMap.put('\uFF7B', "\uFF7B");
- sHalfWidthMap.put('\uFF7C', "\uFF7C");
- sHalfWidthMap.put('\uFF7D', "\uFF7D");
- sHalfWidthMap.put('\uFF7E', "\uFF7E");
- sHalfWidthMap.put('\uFF7F', "\uFF7F");
- sHalfWidthMap.put('\uFF80', "\uFF80");
- sHalfWidthMap.put('\uFF81', "\uFF81");
- sHalfWidthMap.put('\uFF82', "\uFF82");
- sHalfWidthMap.put('\uFF83', "\uFF83");
- sHalfWidthMap.put('\uFF84', "\uFF84");
- sHalfWidthMap.put('\uFF85', "\uFF85");
- sHalfWidthMap.put('\uFF86', "\uFF86");
- sHalfWidthMap.put('\uFF87', "\uFF87");
- sHalfWidthMap.put('\uFF88', "\uFF88");
- sHalfWidthMap.put('\uFF89', "\uFF89");
- sHalfWidthMap.put('\uFF8A', "\uFF8A");
- sHalfWidthMap.put('\uFF8B', "\uFF8B");
- sHalfWidthMap.put('\uFF8C', "\uFF8C");
- sHalfWidthMap.put('\uFF8D', "\uFF8D");
- sHalfWidthMap.put('\uFF8E', "\uFF8E");
- sHalfWidthMap.put('\uFF8F', "\uFF8F");
- sHalfWidthMap.put('\uFF90', "\uFF90");
- sHalfWidthMap.put('\uFF91', "\uFF91");
- sHalfWidthMap.put('\uFF92', "\uFF92");
- sHalfWidthMap.put('\uFF93', "\uFF93");
- sHalfWidthMap.put('\uFF94', "\uFF94");
- sHalfWidthMap.put('\uFF95', "\uFF95");
- sHalfWidthMap.put('\uFF96', "\uFF96");
- sHalfWidthMap.put('\uFF97', "\uFF97");
- sHalfWidthMap.put('\uFF98', "\uFF98");
- sHalfWidthMap.put('\uFF99', "\uFF99");
- sHalfWidthMap.put('\uFF9A', "\uFF9A");
- sHalfWidthMap.put('\uFF9B', "\uFF9B");
- sHalfWidthMap.put('\uFF9C', "\uFF9C");
- sHalfWidthMap.put('\uFF9D', "\uFF9D");
- sHalfWidthMap.put('\uFF9E', "\uFF9E");
- sHalfWidthMap.put('\uFF9F', "\uFF9F");
- sHalfWidthMap.put('\uFFE5', "\u005C\u005C");
- }
-
- /**
- * Return half-width version of that character if possible. Return null if not possible
- * @param ch input character
- * @return CharSequence object if the mapping for ch exists. Return null otherwise.
- */
- public static String tryGetHalfWidthText(char ch) {
- if (sHalfWidthMap.containsKey(ch)) {
- return sHalfWidthMap.get(ch);
- } else {
- return null;
- }
- }
-}
diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java
deleted file mode 100644
index 1da6d7a..0000000
--- a/core/java/android/pim/vcard/VCardBuilder.java
+++ /dev/null
@@ -1,1932 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.pim.vcard;
-
-import android.content.ContentValues;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Event;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.util.CharsetUtils;
-import android.util.Log;
-
-import org.apache.commons.codec.binary.Base64;
-
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.UnsupportedCharsetException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * The class which lets users create their own vCard String.
- */
-public class VCardBuilder {
- private static final String LOG_TAG = "VCardBuilder";
-
- // If you add the other element, please check all the columns are able to be
- // converted to String.
- //
- // e.g. BLOB is not what we can handle here now.
- private static final Set<String> sAllowedAndroidPropertySet =
- Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
- Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
- Relation.CONTENT_ITEM_TYPE)));
-
- public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
- public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
- public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
-
- private static final String VCARD_DATA_VCARD = "VCARD";
- private static final String VCARD_DATA_PUBLIC = "PUBLIC";
-
- private static final String VCARD_PARAM_SEPARATOR = ";";
- private static final String VCARD_END_OF_LINE = "\r\n";
- private static final String VCARD_DATA_SEPARATOR = ":";
- private static final String VCARD_ITEM_SEPARATOR = ";";
- private static final String VCARD_WS = " ";
- private static final String VCARD_PARAM_EQUAL = "=";
-
- private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE";
-
- private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64";
- private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b";
-
- private static final String SHIFT_JIS = "SHIFT_JIS";
- private static final String UTF_8 = "UTF-8";
-
- private final int mVCardType;
-
- private final boolean mIsV30;
- private final boolean mIsJapaneseMobilePhone;
- private final boolean mOnlyOneNoteFieldIsAvailable;
- private final boolean mIsDoCoMo;
- private final boolean mShouldUseQuotedPrintable;
- private final boolean mUsesAndroidProperty;
- private final boolean mUsesDefactProperty;
- private final boolean mUsesUtf8;
- private final boolean mUsesShiftJis;
- private final boolean mAppendTypeParamName;
- private final boolean mRefrainsQPToNameProperties;
- private final boolean mNeedsToConvertPhoneticString;
-
- private final boolean mShouldAppendCharsetParam;
-
- private final String mCharsetString;
- private final String mVCardCharsetParameter;
-
- private StringBuilder mBuilder;
- private boolean mEndAppended;
-
- public VCardBuilder(final int vcardType) {
- mVCardType = vcardType;
-
- mIsV30 = VCardConfig.isV30(vcardType);
- mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType);
- mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
- mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType);
- mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType);
- mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType);
- mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType);
- mUsesUtf8 = VCardConfig.usesUtf8(vcardType);
- mUsesShiftJis = VCardConfig.usesShiftJis(vcardType);
- mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
- mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
- mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
-
- mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8);
-
- if (mIsDoCoMo) {
- String charset;
- try {
- charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
- } catch (UnsupportedCharsetException e) {
- Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
- charset = SHIFT_JIS;
- }
- mCharsetString = charset;
- // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but
- // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in
- // Android, not shown to the public).
- mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
- } else if (mUsesShiftJis) {
- String charset;
- try {
- charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
- } catch (UnsupportedCharsetException e) {
- Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
- charset = SHIFT_JIS;
- }
- mCharsetString = charset;
- mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
- } else {
- mCharsetString = UTF_8;
- mVCardCharsetParameter = "CHARSET=" + UTF_8;
- }
- clear();
- }
-
- public void clear() {
- mBuilder = new StringBuilder();
- mEndAppended = false;
- appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
- if (mIsV30) {
- appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30);
- } else {
- appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21);
- }
- }
-
- private boolean containsNonEmptyName(final ContentValues contentValues) {
- final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
- final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
- final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
- final String prefix = contentValues.getAsString(StructuredName.PREFIX);
- final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
- final String phoneticFamilyName =
- contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
- final String phoneticMiddleName =
- contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
- final String phoneticGivenName =
- contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
- final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
- return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
- TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
- TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
- TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
- TextUtils.isEmpty(displayName));
- }
-
- private ContentValues getPrimaryContentValue(final List<ContentValues> contentValuesList) {
- ContentValues primaryContentValues = null;
- ContentValues subprimaryContentValues = null;
- for (ContentValues contentValues : contentValuesList) {
- if (contentValues == null){
- continue;
- }
- Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
- if (isSuperPrimary != null && isSuperPrimary > 0) {
- // We choose "super primary" ContentValues.
- primaryContentValues = contentValues;
- break;
- } else if (primaryContentValues == null) {
- // We choose the first "primary" ContentValues
- // if "super primary" ContentValues does not exist.
- final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
- if (isPrimary != null && isPrimary > 0 &&
- containsNonEmptyName(contentValues)) {
- primaryContentValues = contentValues;
- // Do not break, since there may be ContentValues with "super primary"
- // afterword.
- } else if (subprimaryContentValues == null &&
- containsNonEmptyName(contentValues)) {
- subprimaryContentValues = contentValues;
- }
- }
- }
-
- if (primaryContentValues == null) {
- if (subprimaryContentValues != null) {
- // We choose the first ContentValues if any "primary" ContentValues does not exist.
- primaryContentValues = subprimaryContentValues;
- } else {
- Log.e(LOG_TAG, "All ContentValues given from database is empty.");
- primaryContentValues = new ContentValues();
- }
- }
-
- return primaryContentValues;
- }
-
- /**
- * For safety, we'll emit just one value around StructuredName, as external importers
- * may get confused with multiple "N", "FN", etc. properties, though it is valid in
- * vCard spec.
- */
- public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) {
- if (contentValuesList == null || contentValuesList.isEmpty()) {
- if (mIsDoCoMo) {
- appendLine(VCardConstants.PROPERTY_N, "");
- } else if (mIsV30) {
- // vCard 3.0 requires "N" and "FN" properties.
- appendLine(VCardConstants.PROPERTY_N, "");
- appendLine(VCardConstants.PROPERTY_FN, "");
- }
- return this;
- }
-
- final ContentValues contentValues = getPrimaryContentValue(contentValuesList);
- final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
- final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
- final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
- final String prefix = contentValues.getAsString(StructuredName.PREFIX);
- final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
- final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
-
- if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
- final boolean reallyAppendCharsetParameterToName =
- shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix);
- final boolean reallyUseQuotedPrintableToName =
- (!mRefrainsQPToNameProperties &&
- !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
- VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
- VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
- VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
- VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
-
- final String formattedName;
- if (!TextUtils.isEmpty(displayName)) {
- formattedName = displayName;
- } else {
- formattedName = VCardUtils.constructNameFromElements(
- VCardConfig.getNameOrderType(mVCardType),
- familyName, middleName, givenName, prefix, suffix);
- }
- final boolean reallyAppendCharsetParameterToFN =
- shouldAppendCharsetParam(formattedName);
- final boolean reallyUseQuotedPrintableToFN =
- !mRefrainsQPToNameProperties &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
-
- final String encodedFamily;
- final String encodedGiven;
- final String encodedMiddle;
- final String encodedPrefix;
- final String encodedSuffix;
- if (reallyUseQuotedPrintableToName) {
- encodedFamily = encodeQuotedPrintable(familyName);
- encodedGiven = encodeQuotedPrintable(givenName);
- encodedMiddle = encodeQuotedPrintable(middleName);
- encodedPrefix = encodeQuotedPrintable(prefix);
- encodedSuffix = encodeQuotedPrintable(suffix);
- } else {
- encodedFamily = escapeCharacters(familyName);
- encodedGiven = escapeCharacters(givenName);
- encodedMiddle = escapeCharacters(middleName);
- encodedPrefix = escapeCharacters(prefix);
- encodedSuffix = escapeCharacters(suffix);
- }
-
- final String encodedFormattedname =
- (reallyUseQuotedPrintableToFN ?
- encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
-
- mBuilder.append(VCardConstants.PROPERTY_N);
- if (mIsDoCoMo) {
- if (reallyAppendCharsetParameterToName) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintableToName) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- // DoCoMo phones require that all the elements in the "family name" field.
- mBuilder.append(formattedName);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- } else {
- if (reallyAppendCharsetParameterToName) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintableToName) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedFamily);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(encodedGiven);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(encodedMiddle);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(encodedPrefix);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(encodedSuffix);
- }
- mBuilder.append(VCARD_END_OF_LINE);
-
- // FN property
- mBuilder.append(VCardConstants.PROPERTY_FN);
- if (reallyAppendCharsetParameterToFN) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintableToFN) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedFormattedname);
- mBuilder.append(VCARD_END_OF_LINE);
- } else if (!TextUtils.isEmpty(displayName)) {
- final boolean reallyUseQuotedPrintableToDisplayName =
- (!mRefrainsQPToNameProperties &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName));
- final String encodedDisplayName =
- reallyUseQuotedPrintableToDisplayName ?
- encodeQuotedPrintable(displayName) :
- escapeCharacters(displayName);
-
- mBuilder.append(VCardConstants.PROPERTY_N);
- if (shouldAppendCharsetParam(displayName)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintableToDisplayName) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedDisplayName);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_END_OF_LINE);
- mBuilder.append(VCardConstants.PROPERTY_FN);
-
- // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
- // when it would be useful for external importers, assuming no external
- // importer allows this vioration.
- if (shouldAppendCharsetParam(displayName)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedDisplayName);
- mBuilder.append(VCARD_END_OF_LINE);
- } else if (mIsV30) {
- // vCard 3.0 specification requires these fields.
- appendLine(VCardConstants.PROPERTY_N, "");
- appendLine(VCardConstants.PROPERTY_FN, "");
- } else if (mIsDoCoMo) {
- appendLine(VCardConstants.PROPERTY_N, "");
- }
-
- appendPhoneticNameFields(contentValues);
- return this;
- }
-
- private void appendPhoneticNameFields(final ContentValues contentValues) {
- final String phoneticFamilyName;
- final String phoneticMiddleName;
- final String phoneticGivenName;
- {
- final String tmpPhoneticFamilyName =
- contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
- final String tmpPhoneticMiddleName =
- contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
- final String tmpPhoneticGivenName =
- contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
- if (mNeedsToConvertPhoneticString) {
- phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
- phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
- phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
- } else {
- phoneticFamilyName = tmpPhoneticFamilyName;
- phoneticMiddleName = tmpPhoneticMiddleName;
- phoneticGivenName = tmpPhoneticGivenName;
- }
- }
-
- if (TextUtils.isEmpty(phoneticFamilyName)
- && TextUtils.isEmpty(phoneticMiddleName)
- && TextUtils.isEmpty(phoneticGivenName)) {
- if (mIsDoCoMo) {
- mBuilder.append(VCardConstants.PROPERTY_SOUND);
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_END_OF_LINE);
- }
- return;
- }
-
- // Try to emit the field(s) related to phonetic name.
- if (mIsV30) {
- final String sortString = VCardUtils
- .constructNameFromElements(mVCardType,
- phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
- mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
- if (shouldAppendCharsetParam(sortString)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(escapeCharacters(sortString));
- mBuilder.append(VCARD_END_OF_LINE);
- } else if (mIsJapaneseMobilePhone) {
- // Note: There is no appropriate property for expressing
- // phonetic name in vCard 2.1, while there is in
- // vCard 3.0 (SORT-STRING).
- // We chose to use DoCoMo's way when the device is Japanese one
- // since it is supported by
- // a lot of Japanese mobile phones. This is "X-" property, so
- // any parser hopefully would not get confused with this.
- //
- // Also, DoCoMo's specification requires vCard composer to use just the first
- // column.
- // i.e.
- // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
- // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;;
- mBuilder.append(VCardConstants.PROPERTY_SOUND);
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
-
- boolean reallyUseQuotedPrintable =
- (!mRefrainsQPToNameProperties
- && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
- phoneticFamilyName)
- && VCardUtils.containsOnlyNonCrLfPrintableAscii(
- phoneticMiddleName)
- && VCardUtils.containsOnlyNonCrLfPrintableAscii(
- phoneticGivenName)));
-
- final String encodedPhoneticFamilyName;
- final String encodedPhoneticMiddleName;
- final String encodedPhoneticGivenName;
- if (reallyUseQuotedPrintable) {
- encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
- encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
- encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
- } else {
- encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
- encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
- encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
- }
-
- if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
- encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- {
- boolean first = true;
- if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
- mBuilder.append(encodedPhoneticFamilyName);
- first = false;
- }
- if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
- if (first) {
- first = false;
- } else {
- mBuilder.append(' ');
- }
- mBuilder.append(encodedPhoneticMiddleName);
- }
- if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
- if (!first) {
- mBuilder.append(' ');
- }
- mBuilder.append(encodedPhoneticGivenName);
- }
- }
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- if (mUsesDefactProperty) {
- if (!TextUtils.isEmpty(phoneticGivenName)) {
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
- final String encodedPhoneticGivenName;
- if (reallyUseQuotedPrintable) {
- encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
- } else {
- encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
- }
- mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
- if (shouldAppendCharsetParam(phoneticGivenName)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedPhoneticGivenName);
- mBuilder.append(VCARD_END_OF_LINE);
- }
- if (!TextUtils.isEmpty(phoneticMiddleName)) {
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
- final String encodedPhoneticMiddleName;
- if (reallyUseQuotedPrintable) {
- encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
- } else {
- encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
- }
- mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
- if (shouldAppendCharsetParam(phoneticMiddleName)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedPhoneticMiddleName);
- mBuilder.append(VCARD_END_OF_LINE);
- }
- if (!TextUtils.isEmpty(phoneticFamilyName)) {
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
- final String encodedPhoneticFamilyName;
- if (reallyUseQuotedPrintable) {
- encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
- } else {
- encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
- }
- mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
- if (shouldAppendCharsetParam(phoneticFamilyName)) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedPhoneticFamilyName);
- mBuilder.append(VCARD_END_OF_LINE);
- }
- }
- }
-
- public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
- final boolean useAndroidProperty;
- if (mIsV30) {
- useAndroidProperty = false;
- } else if (mUsesAndroidProperty) {
- useAndroidProperty = true;
- } else {
- // There's no way to add this field.
- return this;
- }
- if (contentValuesList != null) {
- for (ContentValues contentValues : contentValuesList) {
- final String nickname = contentValues.getAsString(Nickname.NAME);
- if (TextUtils.isEmpty(nickname)) {
- continue;
- }
- if (useAndroidProperty) {
- appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
- } else {
- appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
- }
- }
- }
- return this;
- }
-
- public VCardBuilder appendPhones(final List<ContentValues> contentValuesList) {
- boolean phoneLineExists = false;
- if (contentValuesList != null) {
- Set<String> phoneSet = new HashSet<String>();
- for (ContentValues contentValues : contentValuesList) {
- final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
- final String label = contentValues.getAsString(Phone.LABEL);
- final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
- final boolean isPrimary = (isPrimaryAsInteger != null ?
- (isPrimaryAsInteger > 0) : false);
- String phoneNumber = contentValues.getAsString(Phone.NUMBER);
- if (phoneNumber != null) {
- phoneNumber = phoneNumber.trim();
- }
- if (TextUtils.isEmpty(phoneNumber)) {
- continue;
- }
-
- // PAGER number needs unformatted "phone number".
- final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
- if (type == Phone.TYPE_PAGER ||
- VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
- phoneLineExists = true;
- if (!phoneSet.contains(phoneNumber)) {
- phoneSet.add(phoneNumber);
- appendTelLine(type, label, phoneNumber, isPrimary);
- }
- } else {
- final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber);
- if (phoneNumberList.isEmpty()) {
- continue;
- }
- phoneLineExists = true;
- for (String actualPhoneNumber : phoneNumberList) {
- if (!phoneSet.contains(actualPhoneNumber)) {
- final int format = VCardUtils.getPhoneNumberFormat(mVCardType);
- final String formattedPhoneNumber =
- PhoneNumberUtils.formatNumber(actualPhoneNumber, format);
- phoneSet.add(actualPhoneNumber);
- appendTelLine(type, label, formattedPhoneNumber, isPrimary);
- }
- } // for (String actualPhoneNumber : phoneNumberList) {
- }
- }
- }
-
- if (!phoneLineExists && mIsDoCoMo) {
- appendTelLine(Phone.TYPE_HOME, "", "", false);
- }
-
- return this;
- }
-
- /**
- * <p>
- * Splits a given string expressing phone numbers into several strings, and remove
- * unnecessary characters inside them. The size of a returned list becomes 1 when
- * no split is needed.
- * </p>
- * <p>
- * The given number "may" have several phone numbers when the contact entry is corrupted
- * because of its original source.
- * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
- * </p>
- * <p>
- * This kind of "phone numbers" will not be created with Android vCard implementation,
- * but we may encounter them if the source of the input data has already corrupted
- * implementation.
- * </p>
- * <p>
- * To handle this case, this method first splits its input into multiple parts
- * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
- * removes unnecessary strings like "(Miami)".
- * </p>
- * <p>
- * Do not call this method when trimming is inappropriate for its receivers.
- * </p>
- */
- private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) {
- final List<String> phoneList = new ArrayList<String>();
-
- StringBuilder builder = new StringBuilder();
- final int length = phoneNumber.length();
- for (int i = 0; i < length; i++) {
- final char ch = phoneNumber.charAt(i);
- if (Character.isDigit(ch) || ch == '+') {
- builder.append(ch);
- } else if ((ch == ';' || ch == '\n') && builder.length() > 0) {
- phoneList.add(builder.toString());
- builder = new StringBuilder();
- }
- }
- if (builder.length() > 0) {
- phoneList.add(builder.toString());
- }
-
- return phoneList;
- }
-
- public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
- boolean emailAddressExists = false;
- if (contentValuesList != null) {
- final Set<String> addressSet = new HashSet<String>();
- for (ContentValues contentValues : contentValuesList) {
- String emailAddress = contentValues.getAsString(Email.DATA);
- if (emailAddress != null) {
- emailAddress = emailAddress.trim();
- }
- if (TextUtils.isEmpty(emailAddress)) {
- continue;
- }
- Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
- final int type = (typeAsObject != null ?
- typeAsObject : DEFAULT_EMAIL_TYPE);
- final String label = contentValues.getAsString(Email.LABEL);
- Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
- final boolean isPrimary = (isPrimaryAsInteger != null ?
- (isPrimaryAsInteger > 0) : false);
- emailAddressExists = true;
- if (!addressSet.contains(emailAddress)) {
- addressSet.add(emailAddress);
- appendEmailLine(type, label, emailAddress, isPrimary);
- }
- }
- }
-
- if (!emailAddressExists && mIsDoCoMo) {
- appendEmailLine(Email.TYPE_HOME, "", "", false);
- }
-
- return this;
- }
-
- public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
- if (contentValuesList == null || contentValuesList.isEmpty()) {
- if (mIsDoCoMo) {
- mBuilder.append(VCardConstants.PROPERTY_ADR);
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(VCARD_END_OF_LINE);
- }
- } else {
- if (mIsDoCoMo) {
- appendPostalsForDoCoMo(contentValuesList);
- } else {
- appendPostalsForGeneric(contentValuesList);
- }
- }
-
- return this;
- }
-
- private static final Map<Integer, Integer> sPostalTypePriorityMap;
-
- static {
- sPostalTypePriorityMap = new HashMap<Integer, Integer>();
- sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
- sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
- sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
- sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
- }
-
- /**
- * Tries to append just one line. If there's no appropriate address
- * information, append an empty line.
- */
- private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
- int currentPriority = Integer.MAX_VALUE;
- int currentType = Integer.MAX_VALUE;
- ContentValues currentContentValues = null;
- for (final ContentValues contentValues : contentValuesList) {
- if (contentValues == null) {
- continue;
- }
- final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
- final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
- final int priority =
- (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
- if (priority < currentPriority) {
- currentPriority = priority;
- currentType = typeAsInteger;
- currentContentValues = contentValues;
- if (priority == 0) {
- break;
- }
- }
- }
-
- if (currentContentValues == null) {
- Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
- return;
- }
-
- final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
- appendPostalLine(currentType, label, currentContentValues, false, true);
- }
-
- private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
- for (final ContentValues contentValues : contentValuesList) {
- if (contentValues == null) {
- continue;
- }
- final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
- final int type = (typeAsInteger != null ?
- typeAsInteger : DEFAULT_POSTAL_TYPE);
- final String label = contentValues.getAsString(StructuredPostal.LABEL);
- final Integer isPrimaryAsInteger =
- contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
- final boolean isPrimary = (isPrimaryAsInteger != null ?
- (isPrimaryAsInteger > 0) : false);
- appendPostalLine(type, label, contentValues, isPrimary, false);
- }
- }
-
- private static class PostalStruct {
- final boolean reallyUseQuotedPrintable;
- final boolean appendCharset;
- final String addressData;
- public PostalStruct(final boolean reallyUseQuotedPrintable,
- final boolean appendCharset, final String addressData) {
- this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
- this.appendCharset = appendCharset;
- this.addressData = addressData;
- }
- }
-
- /**
- * @return null when there's no information available to construct the data.
- */
- private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
- // adr-value = 0*6(text-value ";") text-value
- // ; PO Box, Extended Address, Street, Locality, Region, Postal
- // ; Code, Country Name
- final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
- final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
- final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
- final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
- final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
- final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
- final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
- final String[] rawAddressArray = new String[]{
- rawPoBox, rawNeighborhood, rawStreet, rawLocality,
- rawRegion, rawPostalCode, rawCountry};
- if (!VCardUtils.areAllEmpty(rawAddressArray)) {
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
- final boolean appendCharset =
- !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
- final String encodedPoBox;
- final String encodedStreet;
- final String encodedLocality;
- final String encodedRegion;
- final String encodedPostalCode;
- final String encodedCountry;
- final String encodedNeighborhood;
-
- final String rawLocality2;
- // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
- // but this is intentional.
- //
- // QP encoding may add line feeds when needed and the result of
- // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
- // may be different from
- // - encodedLocality + " " + encodedNeighborhood.
- //
- // We use safer way.
- if (TextUtils.isEmpty(rawLocality)) {
- if (TextUtils.isEmpty(rawNeighborhood)) {
- rawLocality2 = "";
- } else {
- rawLocality2 = rawNeighborhood;
- }
- } else {
- if (TextUtils.isEmpty(rawNeighborhood)) {
- rawLocality2 = rawLocality;
- } else {
- rawLocality2 = rawLocality + " " + rawNeighborhood;
- }
- }
- if (reallyUseQuotedPrintable) {
- encodedPoBox = encodeQuotedPrintable(rawPoBox);
- encodedStreet = encodeQuotedPrintable(rawStreet);
- encodedLocality = encodeQuotedPrintable(rawLocality2);
- encodedRegion = encodeQuotedPrintable(rawRegion);
- encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
- encodedCountry = encodeQuotedPrintable(rawCountry);
- } else {
- encodedPoBox = escapeCharacters(rawPoBox);
- encodedStreet = escapeCharacters(rawStreet);
- encodedLocality = escapeCharacters(rawLocality2);
- encodedRegion = escapeCharacters(rawRegion);
- encodedPostalCode = escapeCharacters(rawPostalCode);
- encodedCountry = escapeCharacters(rawCountry);
- encodedNeighborhood = escapeCharacters(rawNeighborhood);
- }
- final StringBuffer addressBuffer = new StringBuffer();
- addressBuffer.append(encodedPoBox);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(encodedStreet);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(encodedLocality);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(encodedRegion);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(encodedPostalCode);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(encodedCountry);
- return new PostalStruct(
- reallyUseQuotedPrintable, appendCharset, addressBuffer.toString());
- } else { // VCardUtils.areAllEmpty(rawAddressArray) == true
- // Try to use FORMATTED_ADDRESS instead.
- final String rawFormattedAddress =
- contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
- if (TextUtils.isEmpty(rawFormattedAddress)) {
- return null;
- }
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
- final boolean appendCharset =
- !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
- final String encodedFormattedAddress;
- if (reallyUseQuotedPrintable) {
- encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
- } else {
- encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
- }
-
- // We use the second value ("Extended Address") just because Japanese mobile phones
- // do so. If the other importer expects the value be in the other field, some flag may
- // be needed.
- final StringBuffer addressBuffer = new StringBuffer();
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(encodedFormattedAddress);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- addressBuffer.append(VCARD_ITEM_SEPARATOR);
- return new PostalStruct(
- reallyUseQuotedPrintable, appendCharset, addressBuffer.toString());
- }
- }
-
- public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
- if (contentValuesList != null) {
- for (ContentValues contentValues : contentValuesList) {
- final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
- if (protocolAsObject == null) {
- continue;
- }
- final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
- if (propertyName == null) {
- continue;
- }
- String data = contentValues.getAsString(Im.DATA);
- if (data != null) {
- data = data.trim();
- }
- if (TextUtils.isEmpty(data)) {
- continue;
- }
- final String typeAsString;
- {
- final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
- switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
- case Im.TYPE_HOME: {
- typeAsString = VCardConstants.PARAM_TYPE_HOME;
- break;
- }
- case Im.TYPE_WORK: {
- typeAsString = VCardConstants.PARAM_TYPE_WORK;
- break;
- }
- case Im.TYPE_CUSTOM: {
- final String label = contentValues.getAsString(Im.LABEL);
- typeAsString = (label != null ? "X-" + label : null);
- break;
- }
- case Im.TYPE_OTHER: // Ignore
- default: {
- typeAsString = null;
- break;
- }
- }
- }
-
- final List<String> parameterList = new ArrayList<String>();
- if (!TextUtils.isEmpty(typeAsString)) {
- parameterList.add(typeAsString);
- }
- final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
- final boolean isPrimary = (isPrimaryAsInteger != null ?
- (isPrimaryAsInteger > 0) : false);
- if (isPrimary) {
- parameterList.add(VCardConstants.PARAM_TYPE_PREF);
- }
-
- appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
- }
- }
- return this;
- }
-
- public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
- if (contentValuesList != null) {
- for (ContentValues contentValues : contentValuesList) {
- String website = contentValues.getAsString(Website.URL);
- if (website != null) {
- website = website.trim();
- }
-
- // Note: vCard 3.0 does not allow any parameter addition toward "URL"
- // property, while there's no document in vCard 2.1.
- if (!TextUtils.isEmpty(website)) {
- appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
- }
- }
- }
- return this;
- }
-
- public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
- if (contentValuesList != null) {
- for (ContentValues contentValues : contentValuesList) {
- String company = contentValues.getAsString(Organization.COMPANY);
- if (company != null) {
- company = company.trim();
- }
- String department = contentValues.getAsString(Organization.DEPARTMENT);
- if (department != null) {
- department = department.trim();
- }
- String title = contentValues.getAsString(Organization.TITLE);
- if (title != null) {
- title = title.trim();
- }
-
- StringBuilder orgBuilder = new StringBuilder();
- if (!TextUtils.isEmpty(company)) {
- orgBuilder.append(company);
- }
- if (!TextUtils.isEmpty(department)) {
- if (orgBuilder.length() > 0) {
- orgBuilder.append(';');
- }
- orgBuilder.append(department);
- }
- final String orgline = orgBuilder.toString();
- appendLine(VCardConstants.PROPERTY_ORG, orgline,
- !VCardUtils.containsOnlyPrintableAscii(orgline),
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
-
- if (!TextUtils.isEmpty(title)) {
- appendLine(VCardConstants.PROPERTY_TITLE, title,
- !VCardUtils.containsOnlyPrintableAscii(title),
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
- }
- }
- }
- return this;
- }
-
- public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
- if (contentValuesList != null) {
- for (ContentValues contentValues : contentValuesList) {
- if (contentValues == null) {
- continue;
- }
- byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
- if (data == null) {
- continue;
- }
- final String photoType = VCardUtils.guessImageType(data);
- if (photoType == null) {
- Log.d(LOG_TAG, "Unknown photo type. Ignored.");
- continue;
- }
- final String photoString = new String(Base64.encodeBase64(data));
- if (!TextUtils.isEmpty(photoString)) {
- appendPhotoLine(photoString, photoType);
- }
- }
- }
- return this;
- }
-
- public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
- if (contentValuesList != null) {
- if (mOnlyOneNoteFieldIsAvailable) {
- final StringBuilder noteBuilder = new StringBuilder();
- boolean first = true;
- for (final ContentValues contentValues : contentValuesList) {
- String note = contentValues.getAsString(Note.NOTE);
- if (note == null) {
- note = "";
- }
- if (note.length() > 0) {
- if (first) {
- first = false;
- } else {
- noteBuilder.append('\n');
- }
- noteBuilder.append(note);
- }
- }
- final String noteStr = noteBuilder.toString();
- // This means we scan noteStr completely twice, which is redundant.
- // But for now, we assume this is not so time-consuming..
- final boolean shouldAppendCharsetInfo =
- !VCardUtils.containsOnlyPrintableAscii(noteStr);
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
- appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
- shouldAppendCharsetInfo, reallyUseQuotedPrintable);
- } else {
- for (ContentValues contentValues : contentValuesList) {
- final String noteStr = contentValues.getAsString(Note.NOTE);
- if (!TextUtils.isEmpty(noteStr)) {
- final boolean shouldAppendCharsetInfo =
- !VCardUtils.containsOnlyPrintableAscii(noteStr);
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
- appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
- shouldAppendCharsetInfo, reallyUseQuotedPrintable);
- }
- }
- }
- }
- return this;
- }
-
- public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
- if (contentValuesList != null) {
- String primaryBirthday = null;
- String secondaryBirthday = null;
- for (final ContentValues contentValues : contentValuesList) {
- if (contentValues == null) {
- continue;
- }
- final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
- final int eventType;
- if (eventTypeAsInteger != null) {
- eventType = eventTypeAsInteger;
- } else {
- eventType = Event.TYPE_OTHER;
- }
- if (eventType == Event.TYPE_BIRTHDAY) {
- final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
- if (birthdayCandidate == null) {
- continue;
- }
- final Integer isSuperPrimaryAsInteger =
- contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
- final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
- (isSuperPrimaryAsInteger > 0) : false);
- if (isSuperPrimary) {
- // "super primary" birthday should the prefered one.
- primaryBirthday = birthdayCandidate;
- break;
- }
- final Integer isPrimaryAsInteger =
- contentValues.getAsInteger(Event.IS_PRIMARY);
- final boolean isPrimary = (isPrimaryAsInteger != null ?
- (isPrimaryAsInteger > 0) : false);
- if (isPrimary) {
- // We don't break here since "super primary" birthday may exist later.
- primaryBirthday = birthdayCandidate;
- } else if (secondaryBirthday == null) {
- // First entry is set to the "secondary" candidate.
- secondaryBirthday = birthdayCandidate;
- }
- } else if (mUsesAndroidProperty) {
- // Event types other than Birthday is not supported by vCard.
- appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
- }
- }
- if (primaryBirthday != null) {
- appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
- primaryBirthday.trim());
- } else if (secondaryBirthday != null){
- appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
- secondaryBirthday.trim());
- }
- }
- return this;
- }
-
- public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
- if (mUsesAndroidProperty && contentValuesList != null) {
- for (final ContentValues contentValues : contentValuesList) {
- if (contentValues == null) {
- continue;
- }
- appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
- }
- }
- return this;
- }
-
- public void appendPostalLine(final int type, final String label,
- final ContentValues contentValues,
- final boolean isPrimary, final boolean emitLineEveryTime) {
- final boolean reallyUseQuotedPrintable;
- final boolean appendCharset;
- final String addressValue;
- {
- PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
- if (postalStruct == null) {
- if (emitLineEveryTime) {
- reallyUseQuotedPrintable = false;
- appendCharset = false;
- addressValue = "";
- } else {
- return;
- }
- } else {
- reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
- appendCharset = postalStruct.appendCharset;
- addressValue = postalStruct.addressData;
- }
- }
-
- List<String> parameterList = new ArrayList<String>();
- if (isPrimary) {
- parameterList.add(VCardConstants.PARAM_TYPE_PREF);
- }
- switch (type) {
- case StructuredPostal.TYPE_HOME: {
- parameterList.add(VCardConstants.PARAM_TYPE_HOME);
- break;
- }
- case StructuredPostal.TYPE_WORK: {
- parameterList.add(VCardConstants.PARAM_TYPE_WORK);
- break;
- }
- case StructuredPostal.TYPE_CUSTOM: {
- if (!TextUtils.isEmpty(label)
- && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
- // We're not sure whether the label is valid in the spec
- // ("IANA-token" in the vCard 3.0 is unclear...)
- // Just for safety, we add "X-" at the beggining of each label.
- // Also checks the label obeys with vCard 3.0 spec.
- parameterList.add("X-" + label);
- }
- break;
- }
- case StructuredPostal.TYPE_OTHER: {
- break;
- }
- default: {
- Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
- break;
- }
- }
-
- mBuilder.append(VCardConstants.PROPERTY_ADR);
- if (!parameterList.isEmpty()) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- appendTypeParameters(parameterList);
- }
- if (appendCharset) {
- // Strictly, vCard 3.0 does not allow exporters to emit charset information,
- // but we will add it since the information should be useful for importers,
- //
- // Assume no parser does not emit error with this parameter in vCard 3.0.
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(addressValue);
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- public void appendEmailLine(final int type, final String label,
- final String rawValue, final boolean isPrimary) {
- final String typeAsString;
- switch (type) {
- case Email.TYPE_CUSTOM: {
- if (VCardUtils.isMobilePhoneLabel(label)) {
- typeAsString = VCardConstants.PARAM_TYPE_CELL;
- } else if (!TextUtils.isEmpty(label)
- && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
- typeAsString = "X-" + label;
- } else {
- typeAsString = null;
- }
- break;
- }
- case Email.TYPE_HOME: {
- typeAsString = VCardConstants.PARAM_TYPE_HOME;
- break;
- }
- case Email.TYPE_WORK: {
- typeAsString = VCardConstants.PARAM_TYPE_WORK;
- break;
- }
- case Email.TYPE_OTHER: {
- typeAsString = null;
- break;
- }
- case Email.TYPE_MOBILE: {
- typeAsString = VCardConstants.PARAM_TYPE_CELL;
- break;
- }
- default: {
- Log.e(LOG_TAG, "Unknown Email type: " + type);
- typeAsString = null;
- break;
- }
- }
-
- final List<String> parameterList = new ArrayList<String>();
- if (isPrimary) {
- parameterList.add(VCardConstants.PARAM_TYPE_PREF);
- }
- if (!TextUtils.isEmpty(typeAsString)) {
- parameterList.add(typeAsString);
- }
-
- appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
- rawValue);
- }
-
- public void appendTelLine(final Integer typeAsInteger, final String label,
- final String encodedValue, boolean isPrimary) {
- mBuilder.append(VCardConstants.PROPERTY_TEL);
- mBuilder.append(VCARD_PARAM_SEPARATOR);
-
- final int type;
- if (typeAsInteger == null) {
- type = Phone.TYPE_OTHER;
- } else {
- type = typeAsInteger;
- }
-
- ArrayList<String> parameterList = new ArrayList<String>();
- switch (type) {
- case Phone.TYPE_HOME: {
- parameterList.addAll(
- Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
- break;
- }
- case Phone.TYPE_WORK: {
- parameterList.addAll(
- Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
- break;
- }
- case Phone.TYPE_FAX_HOME: {
- parameterList.addAll(
- Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
- break;
- }
- case Phone.TYPE_FAX_WORK: {
- parameterList.addAll(
- Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
- break;
- }
- case Phone.TYPE_MOBILE: {
- parameterList.add(VCardConstants.PARAM_TYPE_CELL);
- break;
- }
- case Phone.TYPE_PAGER: {
- if (mIsDoCoMo) {
- // Not sure about the reason, but previous implementation had
- // used "VOICE" instead of "PAGER"
- parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
- } else {
- parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
- }
- break;
- }
- case Phone.TYPE_OTHER: {
- parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
- break;
- }
- case Phone.TYPE_CAR: {
- parameterList.add(VCardConstants.PARAM_TYPE_CAR);
- break;
- }
- case Phone.TYPE_COMPANY_MAIN: {
- // There's no relevant field in vCard (at least 2.1).
- parameterList.add(VCardConstants.PARAM_TYPE_WORK);
- isPrimary = true;
- break;
- }
- case Phone.TYPE_ISDN: {
- parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
- break;
- }
- case Phone.TYPE_MAIN: {
- isPrimary = true;
- break;
- }
- case Phone.TYPE_OTHER_FAX: {
- parameterList.add(VCardConstants.PARAM_TYPE_FAX);
- break;
- }
- case Phone.TYPE_TELEX: {
- parameterList.add(VCardConstants.PARAM_TYPE_TLX);
- break;
- }
- case Phone.TYPE_WORK_MOBILE: {
- parameterList.addAll(
- Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
- break;
- }
- case Phone.TYPE_WORK_PAGER: {
- parameterList.add(VCardConstants.PARAM_TYPE_WORK);
- // See above.
- if (mIsDoCoMo) {
- parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
- } else {
- parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
- }
- break;
- }
- case Phone.TYPE_MMS: {
- parameterList.add(VCardConstants.PARAM_TYPE_MSG);
- break;
- }
- case Phone.TYPE_CUSTOM: {
- if (TextUtils.isEmpty(label)) {
- // Just ignore the custom type.
- parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
- } else if (VCardUtils.isMobilePhoneLabel(label)) {
- parameterList.add(VCardConstants.PARAM_TYPE_CELL);
- } else {
- final String upperLabel = label.toUpperCase();
- if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
- parameterList.add(upperLabel);
- } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
- // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
- // "TYPE=" string.
- parameterList.add("X-" + label);
- }
- }
- break;
- }
- case Phone.TYPE_RADIO:
- case Phone.TYPE_TTY_TDD:
- default: {
- break;
- }
- }
-
- if (isPrimary) {
- parameterList.add(VCardConstants.PARAM_TYPE_PREF);
- }
-
- if (parameterList.isEmpty()) {
- appendUncommonPhoneType(mBuilder, type);
- } else {
- appendTypeParameters(parameterList);
- }
-
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedValue);
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- /**
- * Appends phone type string which may not be available in some devices.
- */
- private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
- if (mIsDoCoMo) {
- // The previous implementation for DoCoMo had been conservative
- // about miscellaneous types.
- builder.append(VCardConstants.PARAM_TYPE_VOICE);
- } else {
- String phoneType = VCardUtils.getPhoneTypeString(type);
- if (phoneType != null) {
- appendTypeParameter(phoneType);
- } else {
- Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
- }
- }
- }
-
- /**
- * @param encodedValue Must be encoded by BASE64
- * @param photoType
- */
- public void appendPhotoLine(final String encodedValue, final String photoType) {
- StringBuilder tmpBuilder = new StringBuilder();
- tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
- tmpBuilder.append(VCARD_PARAM_SEPARATOR);
- if (mIsV30) {
- tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30);
- } else {
- tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
- }
- tmpBuilder.append(VCARD_PARAM_SEPARATOR);
- appendTypeParameter(tmpBuilder, photoType);
- tmpBuilder.append(VCARD_DATA_SEPARATOR);
- tmpBuilder.append(encodedValue);
-
- final String tmpStr = tmpBuilder.toString();
- tmpBuilder = new StringBuilder();
- int lineCount = 0;
- final int length = tmpStr.length();
- final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
- - VCARD_END_OF_LINE.length();
- final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
- int maxNum = maxNumForFirstLine;
- for (int i = 0; i < length; i++) {
- tmpBuilder.append(tmpStr.charAt(i));
- lineCount++;
- if (lineCount > maxNum) {
- tmpBuilder.append(VCARD_END_OF_LINE);
- tmpBuilder.append(VCARD_WS);
- maxNum = maxNumInGeneral;
- lineCount = 0;
- }
- }
- mBuilder.append(tmpBuilder.toString());
- mBuilder.append(VCARD_END_OF_LINE);
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) {
- if (!sAllowedAndroidPropertySet.contains(mimeType)) {
- return;
- }
- final List<String> rawValueList = new ArrayList<String>();
- for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
- String value = contentValues.getAsString("data" + i);
- if (value == null) {
- value = "";
- }
- rawValueList.add(value);
- }
-
- boolean needCharset =
- (mShouldAppendCharsetParam &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
- boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
- mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
- if (needCharset) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (reallyUseQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(mimeType); // Should not be encoded.
- for (String rawValue : rawValueList) {
- final String encodedValue;
- if (reallyUseQuotedPrintable) {
- encodedValue = encodeQuotedPrintable(rawValue);
- } else {
- // TODO: one line may be too huge, which may be invalid in vCard 3.0
- // (which says "When generating a content line, lines longer than
- // 75 characters SHOULD be folded"), though several
- // (even well-known) applications do not care this.
- encodedValue = escapeCharacters(rawValue);
- }
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- mBuilder.append(encodedValue);
- }
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- public void appendLineWithCharsetAndQPDetection(final String propertyName,
- final String rawValue) {
- appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
- }
-
- public void appendLineWithCharsetAndQPDetection(
- final String propertyName, final List<String> rawValueList) {
- appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
- }
-
- public void appendLineWithCharsetAndQPDetection(final String propertyName,
- final List<String> parameterList, final String rawValue) {
- final boolean needCharset =
- !VCardUtils.containsOnlyPrintableAscii(rawValue);
- final boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
- appendLine(propertyName, parameterList,
- rawValue, needCharset, reallyUseQuotedPrintable);
- }
-
- public void appendLineWithCharsetAndQPDetection(final String propertyName,
- final List<String> parameterList, final List<String> rawValueList) {
- boolean needCharset =
- (mShouldAppendCharsetParam &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
- boolean reallyUseQuotedPrintable =
- (mShouldUseQuotedPrintable &&
- !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
- appendLine(propertyName, parameterList, rawValueList,
- needCharset, reallyUseQuotedPrintable);
- }
-
- /**
- * Appends one line with a given property name and value.
- */
- public void appendLine(final String propertyName, final String rawValue) {
- appendLine(propertyName, rawValue, false, false);
- }
-
- public void appendLine(final String propertyName, final List<String> rawValueList) {
- appendLine(propertyName, rawValueList, false, false);
- }
-
- public void appendLine(final String propertyName,
- final String rawValue, final boolean needCharset,
- boolean reallyUseQuotedPrintable) {
- appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
- }
-
- public void appendLine(final String propertyName, final List<String> parameterList,
- final String rawValue) {
- appendLine(propertyName, parameterList, rawValue, false, false);
- }
-
- public void appendLine(final String propertyName, final List<String> parameterList,
- final String rawValue, final boolean needCharset,
- boolean reallyUseQuotedPrintable) {
- mBuilder.append(propertyName);
- if (parameterList != null && parameterList.size() > 0) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- appendTypeParameters(parameterList);
- }
- if (needCharset) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
-
- final String encodedValue;
- if (reallyUseQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- encodedValue = encodeQuotedPrintable(rawValue);
- } else {
- // TODO: one line may be too huge, which may be invalid in vCard spec, though
- // several (even well-known) applications do not care this.
- encodedValue = escapeCharacters(rawValue);
- }
-
- mBuilder.append(VCARD_DATA_SEPARATOR);
- mBuilder.append(encodedValue);
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- public void appendLine(final String propertyName, final List<String> rawValueList,
- final boolean needCharset, boolean needQuotedPrintable) {
- appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
- }
-
- public void appendLine(final String propertyName, final List<String> parameterList,
- final List<String> rawValueList, final boolean needCharset,
- final boolean needQuotedPrintable) {
- mBuilder.append(propertyName);
- if (parameterList != null && parameterList.size() > 0) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- appendTypeParameters(parameterList);
- }
- if (needCharset) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(mVCardCharsetParameter);
- }
- if (needQuotedPrintable) {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- mBuilder.append(VCARD_PARAM_ENCODING_QP);
- }
-
- mBuilder.append(VCARD_DATA_SEPARATOR);
- boolean first = true;
- for (String rawValue : rawValueList) {
- final String encodedValue;
- if (needQuotedPrintable) {
- encodedValue = encodeQuotedPrintable(rawValue);
- } else {
- // TODO: one line may be too huge, which may be invalid in vCard 3.0
- // (which says "When generating a content line, lines longer than
- // 75 characters SHOULD be folded"), though several
- // (even well-known) applications do not care this.
- encodedValue = escapeCharacters(rawValue);
- }
-
- if (first) {
- first = false;
- } else {
- mBuilder.append(VCARD_ITEM_SEPARATOR);
- }
- mBuilder.append(encodedValue);
- }
- mBuilder.append(VCARD_END_OF_LINE);
- }
-
- /**
- * VCARD_PARAM_SEPARATOR must be appended before this method being called.
- */
- private void appendTypeParameters(final List<String> types) {
- // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
- // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
- boolean first = true;
- for (final String typeValue : types) {
- // Note: vCard 3.0 specifies the different type of acceptable type Strings, but
- // we don't emit that kind of vCard 3.0 specific type since there should be
- // high probabilyty in which external importers cannot understand them.
- //
- // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they
- // are quoted.)
- if (!VCardUtils.isV21Word(typeValue)) {
- continue;
- }
- if (first) {
- first = false;
- } else {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- }
- appendTypeParameter(typeValue);
- }
- }
-
- /**
- * VCARD_PARAM_SEPARATOR must be appended before this method being called.
- */
- private void appendTypeParameter(final String type) {
- appendTypeParameter(mBuilder, type);
- }
-
- private void appendTypeParameter(final StringBuilder builder, final String type) {
- // Refrain from using appendType() so that "TYPE=" is not be appended when the
- // device is DoCoMo's (just for safety).
- //
- // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
- if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) {
- builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
- }
- builder.append(type);
- }
-
- /**
- * Returns true when the property line should contain charset parameter
- * information. This method may return true even when vCard version is 3.0.
- *
- * Strictly, adding charset information is invalid in VCard 3.0.
- * However we'll add the info only when charset we use is not UTF-8
- * in vCard 3.0 format, since parser side may be able to use the charset
- * via this field, though we may encounter another problem by adding it.
- *
- * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
- * recommends UTF-8. By adding this field, parsers may be able
- * to know this text is NOT UTF-8 but Shift_Jis.
- */
- private boolean shouldAppendCharsetParam(String...propertyValueList) {
- if (!mShouldAppendCharsetParam) {
- return false;
- }
- for (String propertyValue : propertyValueList) {
- if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
- return true;
- }
- }
- return false;
- }
-
- private String encodeQuotedPrintable(final String str) {
- if (TextUtils.isEmpty(str)) {
- return "";
- }
-
- final StringBuilder builder = new StringBuilder();
- int index = 0;
- int lineCount = 0;
- byte[] strArray = null;
-
- try {
- strArray = str.getBytes(mCharsetString);
- } catch (UnsupportedEncodingException e) {
- Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. "
- + "Try default charset");
- strArray = str.getBytes();
- }
- while (index < strArray.length) {
- builder.append(String.format("=%02X", strArray[index]));
- index += 1;
- lineCount += 3;
-
- if (lineCount >= 67) {
- // Specification requires CRLF must be inserted before the
- // length of the line
- // becomes more than 76.
- // Assuming that the next character is a multi-byte character,
- // it will become
- // 6 bytes.
- // 76 - 6 - 3 = 67
- builder.append("=\r\n");
- lineCount = 0;
- }
- }
-
- return builder.toString();
- }
-
- /**
- * Append '\' to the characters which should be escaped. The character set is different
- * not only between vCard 2.1 and vCard 3.0 but also among each device.
- *
- * Note that Quoted-Printable string must not be input here.
- */
- @SuppressWarnings("fallthrough")
- private String escapeCharacters(final String unescaped) {
- if (TextUtils.isEmpty(unescaped)) {
- return "";
- }
-
- final StringBuilder tmpBuilder = new StringBuilder();
- final int length = unescaped.length();
- for (int i = 0; i < length; i++) {
- final char ch = unescaped.charAt(i);
- switch (ch) {
- case ';': {
- tmpBuilder.append('\\');
- tmpBuilder.append(';');
- break;
- }
- case '\r': {
- if (i + 1 < length) {
- char nextChar = unescaped.charAt(i);
- if (nextChar == '\n') {
- break;
- } else {
- // fall through
- }
- } else {
- // fall through
- }
- }
- case '\n': {
- // In vCard 2.1, there's no specification about this, while
- // vCard 3.0 explicitly requires this should be encoded to "\n".
- tmpBuilder.append("\\n");
- break;
- }
- case '\\': {
- if (mIsV30) {
- tmpBuilder.append("\\\\");
- break;
- } else {
- // fall through
- }
- }
- case '<':
- case '>': {
- if (mIsDoCoMo) {
- tmpBuilder.append('\\');
- tmpBuilder.append(ch);
- } else {
- tmpBuilder.append(ch);
- }
- break;
- }
- case ',': {
- if (mIsV30) {
- tmpBuilder.append("\\,");
- } else {
- tmpBuilder.append(ch);
- }
- break;
- }
- default: {
- tmpBuilder.append(ch);
- break;
- }
- }
- }
- return tmpBuilder.toString();
- }
-
- @Override
- public String toString() {
- if (!mEndAppended) {
- if (mIsDoCoMo) {
- appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
- appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
- appendLine(VCardConstants.PROPERTY_X_NO, "");
- appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
- }
- appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
- mEndAppended = true;
- }
- return mBuilder.toString();
- }
-}
diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java
deleted file mode 100644
index 0e8b665..0000000
--- a/core/java/android/pim/vcard/VCardComposer.java
+++ /dev/null
@@ -1,592 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.pim.vcard;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.content.EntityIterator;
-import android.content.Entity.NamedContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.net.Uri;
-import android.pim.vcard.exception.VCardException;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.RawContactsEntity;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Event;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.util.CharsetUtils;
-import android.util.Log;
-
-import java.io.BufferedWriter;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.nio.charset.UnsupportedCharsetException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * <p>
- * The class for composing VCard from Contacts information. Note that this is
- * completely differnt implementation from
- * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore.
- * </p>
- *
- * <p>
- * Usually, this class should be used like this.
- * </p>
- *
- * <pre class="prettyprint">VCardComposer composer = null;
- * try {
- * composer = new VCardComposer(context);
- * composer.addHandler(
- * composer.new HandlerForOutputStream(outputStream));
- * if (!composer.init()) {
- * // Do something handling the situation.
- * return;
- * }
- * while (!composer.isAfterLast()) {
- * if (mCanceled) {
- * // Assume a user may cancel this operation during the export.
- * return;
- * }
- * if (!composer.createOneEntry()) {
- * // Do something handling the error situation.
- * return;
- * }
- * }
- * } finally {
- * if (composer != null) {
- * composer.terminate();
- * }
- * } </pre>
- */
-public class VCardComposer {
- private static final String LOG_TAG = "VCardComposer";
-
- public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
- public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
- public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
-
- public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
- "Failed to get database information";
-
- public static final String FAILURE_REASON_NO_ENTRY =
- "There's no exportable in the database";
-
- public static final String FAILURE_REASON_NOT_INITIALIZED =
- "The vCard composer object is not correctly initialized";
-
- /** Should be visible only from developers... (no need to translate, hopefully) */
- public static final String FAILURE_REASON_UNSUPPORTED_URI =
- "The Uri vCard composer received is not supported by the composer.";
-
- public static final String NO_ERROR = "No error";
-
- public static final String VCARD_TYPE_STRING_DOCOMO = "docomo";
-
- private static final String SHIFT_JIS = "SHIFT_JIS";
- private static final String UTF_8 = "UTF-8";
-
- /**
- * Special URI for testing.
- */
- public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
- public static final Uri VCARD_TEST_AUTHORITY_URI =
- Uri.parse("content://" + VCARD_TEST_AUTHORITY);
- public static final Uri CONTACTS_TEST_CONTENT_URI =
- Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
-
- private static final Map<Integer, String> sImMap;
-
- static {
- sImMap = new HashMap<Integer, String>();
- sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
- sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
- sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
- sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
- sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
- sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
- // Google talk is a special case.
- }
-
- public static interface OneEntryHandler {
- public boolean onInit(Context context);
- public boolean onEntryCreated(String vcard);
- public void onTerminate();
- }
-
- /**
- * <p>
- * An useful example handler, which emits VCard String to outputstream one by one.
- * </p>
- * <p>
- * The input OutputStream object is closed() on {@link #onTerminate()}.
- * Must not close the stream outside.
- * </p>
- */
- public class HandlerForOutputStream implements OneEntryHandler {
- @SuppressWarnings("hiding")
- private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream";
-
- final private OutputStream mOutputStream; // mWriter will close this.
- private Writer mWriter;
-
- private boolean mOnTerminateIsCalled = false;
-
- /**
- * Input stream will be closed on the detruction of this object.
- */
- public HandlerForOutputStream(OutputStream outputStream) {
- mOutputStream = outputStream;
- }
-
- public boolean onInit(Context context) {
- try {
- mWriter = new BufferedWriter(new OutputStreamWriter(
- mOutputStream, mCharsetString));
- } catch (UnsupportedEncodingException e1) {
- Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString);
- mErrorReason = "Encoding is not supported (usually this does not happen!): "
- + mCharsetString;
- return false;
- }
-
- if (mIsDoCoMo) {
- try {
- // Create one empty entry.
- mWriter.write(createOneEntryInternal("-1", null));
- } catch (VCardException e) {
- Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " +
- e.getMessage());
- return false;
- } catch (IOException e) {
- Log.e(LOG_TAG,
- "IOException occurred during exportOneContactData: "
- + e.getMessage());
- mErrorReason = "IOException occurred: " + e.getMessage();
- return false;
- }
- }
- return true;
- }
-
- public boolean onEntryCreated(String vcard) {
- try {
- mWriter.write(vcard);
- } catch (IOException e) {
- Log.e(LOG_TAG,
- "IOException occurred during exportOneContactData: "
- + e.getMessage());
- mErrorReason = "IOException occurred: " + e.getMessage();
- return false;
- }
- return true;
- }
-
- public void onTerminate() {
- mOnTerminateIsCalled = true;
- if (mWriter != null) {
- try {
- // Flush and sync the data so that a user is able to pull
- // the SDCard just after
- // the export.
- mWriter.flush();
- if (mOutputStream != null
- && mOutputStream instanceof FileOutputStream) {
- ((FileOutputStream) mOutputStream).getFD().sync();
- }
- } catch (IOException e) {
- Log.d(LOG_TAG,
- "IOException during closing the output stream: "
- + e.getMessage());
- } finally {
- try {
- mWriter.close();
- } catch (IOException e) {
- }
- }
- }
- }
-
- @Override
- public void finalize() {
- if (!mOnTerminateIsCalled) {
- onTerminate();
- }
- }
- }
-
- private final Context mContext;
- private final int mVCardType;
- private final boolean mCareHandlerErrors;
- private final ContentResolver mContentResolver;
-
- private final boolean mIsDoCoMo;
- private final boolean mUsesShiftJis;
- private Cursor mCursor;
- private int mIdColumn;
-
- private final String mCharsetString;
- private boolean mTerminateIsCalled;
- private final List<OneEntryHandler> mHandlerList;
-
- private String mErrorReason = NO_ERROR;
-
- private static final String[] sContactsProjection = new String[] {
- Contacts._ID,
- };
-
- public VCardComposer(Context context) {
- this(context, VCardConfig.VCARD_TYPE_DEFAULT, true);
- }
-
- public VCardComposer(Context context, int vcardType) {
- this(context, vcardType, true);
- }
-
- public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) {
- this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors);
- }
-
- /**
- * Construct for supporting call log entry vCard composing.
- */
- public VCardComposer(final Context context, final int vcardType,
- final boolean careHandlerErrors) {
- mContext = context;
- mVCardType = vcardType;
- mCareHandlerErrors = careHandlerErrors;
- mContentResolver = context.getContentResolver();
-
- mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
- mUsesShiftJis = VCardConfig.usesShiftJis(vcardType);
- mHandlerList = new ArrayList<OneEntryHandler>();
-
- if (mIsDoCoMo) {
- String charset;
- try {
- charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
- } catch (UnsupportedCharsetException e) {
- Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
- charset = SHIFT_JIS;
- }
- mCharsetString = charset;
- } else if (mUsesShiftJis) {
- String charset;
- try {
- charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
- } catch (UnsupportedCharsetException e) {
- Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
- charset = SHIFT_JIS;
- }
- mCharsetString = charset;
- } else {
- mCharsetString = UTF_8;
- }
- }
-
- /**
- * Must be called before {@link #init()}.
- */
- public void addHandler(OneEntryHandler handler) {
- if (handler != null) {
- mHandlerList.add(handler);
- }
- }
-
- /**
- * @return Returns true when initialization is successful and all the other
- * methods are available. Returns false otherwise.
- */
- public boolean init() {
- return init(null, null);
- }
-
- public boolean init(final String selection, final String[] selectionArgs) {
- return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
- }
-
- /**
- * Note that this is unstable interface, may be deleted in the future.
- */
- public boolean init(final Uri contentUri, final String selection,
- final String[] selectionArgs, final String sortOrder) {
- if (contentUri == null) {
- return false;
- }
-
- if (mCareHandlerErrors) {
- List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
- mHandlerList.size());
- for (OneEntryHandler handler : mHandlerList) {
- if (!handler.onInit(mContext)) {
- for (OneEntryHandler finished : finishedList) {
- finished.onTerminate();
- }
- return false;
- }
- }
- } else {
- // Just ignore the false returned from onInit().
- for (OneEntryHandler handler : mHandlerList) {
- handler.onInit(mContext);
- }
- }
-
- final String[] projection;
- if (Contacts.CONTENT_URI.equals(contentUri) ||
- CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
- projection = sContactsProjection;
- } else {
- mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
- return false;
- }
- mCursor = mContentResolver.query(
- contentUri, projection, selection, selectionArgs, sortOrder);
-
- if (mCursor == null) {
- mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
- return false;
- }
-
- if (getCount() == 0 || !mCursor.moveToFirst()) {
- try {
- mCursor.close();
- } catch (SQLiteException e) {
- Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
- } finally {
- mCursor = null;
- mErrorReason = FAILURE_REASON_NO_ENTRY;
- }
- return false;
- }
-
- mIdColumn = mCursor.getColumnIndex(Contacts._ID);
-
- return true;
- }
-
- public boolean createOneEntry() {
- return createOneEntry(null);
- }
-
- /**
- * @param getEntityIteratorMethod For Dependency Injection.
- * @hide just for testing.
- */
- public boolean createOneEntry(Method getEntityIteratorMethod) {
- if (mCursor == null || mCursor.isAfterLast()) {
- mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
- return false;
- }
- String vcard;
- try {
- if (mIdColumn >= 0) {
- vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
- getEntityIteratorMethod);
- } else {
- Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
- return true;
- }
- } catch (VCardException e) {
- Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage());
- return false;
- } catch (OutOfMemoryError error) {
- // Maybe some data (e.g. photo) is too big to have in memory. But it
- // should be rare.
- Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry.");
- System.gc();
- // TODO: should tell users what happened?
- return true;
- } finally {
- mCursor.moveToNext();
- }
-
- // This function does not care the OutOfMemoryError on the handler side
- // :-P
- if (mCareHandlerErrors) {
- List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
- mHandlerList.size());
- for (OneEntryHandler handler : mHandlerList) {
- if (!handler.onEntryCreated(vcard)) {
- return false;
- }
- }
- } else {
- for (OneEntryHandler handler : mHandlerList) {
- handler.onEntryCreated(vcard);
- }
- }
-
- return true;
- }
-
- private String createOneEntryInternal(final String contactId,
- Method getEntityIteratorMethod) throws VCardException {
- final Map<String, List<ContentValues>> contentValuesListMap =
- new HashMap<String, List<ContentValues>>();
- // The resolver may return the entity iterator with no data. It is possible.
- // e.g. If all the data in the contact of the given contact id are not exportable ones,
- // they are hidden from the view of this method, though contact id itself exists.
- EntityIterator entityIterator = null;
- try {
- final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
- .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1")
- .build();
- final String selection = Data.CONTACT_ID + "=?";
- final String[] selectionArgs = new String[] {contactId};
- if (getEntityIteratorMethod != null) {
- // Please note that this branch is executed by some tests only
- try {
- entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
- mContentResolver, uri, selection, selectionArgs, null);
- } catch (IllegalArgumentException e) {
- Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
- e.getMessage());
- } catch (IllegalAccessException e) {
- Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
- e.getMessage());
- } catch (InvocationTargetException e) {
- Log.e(LOG_TAG, "InvocationTargetException has been thrown: ");
- StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
- for (StackTraceElement element : stackTraceElements) {
- Log.e(LOG_TAG, " at " + element.toString());
- }
- throw new VCardException("InvocationTargetException has been thrown: " +
- e.getCause().getMessage());
- }
- } else {
- entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
- uri, null, selection, selectionArgs, null));
- }
-
- if (entityIterator == null) {
- Log.e(LOG_TAG, "EntityIterator is null");
- return "";
- }
-
- if (!entityIterator.hasNext()) {
- Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
- return "";
- }
-
- while (entityIterator.hasNext()) {
- Entity entity = entityIterator.next();
- for (NamedContentValues namedContentValues : entity.getSubValues()) {
- ContentValues contentValues = namedContentValues.values;
- String key = contentValues.getAsString(Data.MIMETYPE);
- if (key != null) {
- List<ContentValues> contentValuesList =
- contentValuesListMap.get(key);
- if (contentValuesList == null) {
- contentValuesList = new ArrayList<ContentValues>();
- contentValuesListMap.put(key, contentValuesList);
- }
- contentValuesList.add(contentValues);
- }
- }
- }
- } finally {
- if (entityIterator != null) {
- entityIterator.close();
- }
- }
-
- final VCardBuilder builder = new VCardBuilder(mVCardType);
- builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
- .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
- .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
- .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
- .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
- .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
- .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
- if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
- builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
- }
- builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
- .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
- .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
- .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
- return builder.toString();
- }
-
- public void terminate() {
- for (OneEntryHandler handler : mHandlerList) {
- handler.onTerminate();
- }
-
- if (mCursor != null) {
- try {
- mCursor.close();
- } catch (SQLiteException e) {
- Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
- }
- mCursor = null;
- }
-
- mTerminateIsCalled = true;
- }
-
- @Override
- public void finalize() {
- if (!mTerminateIsCalled) {
- terminate();
- }
- }
-
- public int getCount() {
- if (mCursor == null) {
- return 0;
- }
- return mCursor.getCount();
- }
-
- public boolean isAfterLast() {
- if (mCursor == null) {
- return false;
- }
- return mCursor.isAfterLast();
- }
-
- /**
- * @return Return the error reason if possible.
- */
- public String getErrorReason() {
- return mErrorReason;
- }
-}
diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java
deleted file mode 100644
index 8219840..0000000
--- a/core/java/android/pim/vcard/VCardConfig.java
+++ /dev/null
@@ -1,477 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.telephony.PhoneNumberUtils;
-import android.util.Log;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * The class representing VCard related configurations. Useful static methods are not in this class
- * but in VCardUtils.
- */
-public class VCardConfig {
- private static final String LOG_TAG = "VCardConfig";
-
- /* package */ static final int LOG_LEVEL_NONE = 0;
- /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1;
- /* package */ static final int LOG_LEVEL_SHOW_WARNING = 0x2;
- /* package */ static final int LOG_LEVEL_VERBOSE =
- LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING;
-
- /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE;
-
- /* package */ static final int PARSE_TYPE_UNKNOWN = 0;
- /* package */ static final int PARSE_TYPE_APPLE = 1;
- /* package */ static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; // For Japanese mobile phones.
- /* package */ static final int PARSE_TYPE_FOMA = 3; // For Japanese FOMA mobile phones.
- /* package */ static final int PARSE_TYPE_WINDOWS_MOBILE_JP = 4;
-
- // Assumes 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 final String DEFAULT_CHARSET = "iso-8859-1";
-
- public static final int FLAG_V21 = 0;
- public static final int FLAG_V30 = 1;
-
- // 0x2 is reserved for the future use ...
-
- public static final int NAME_ORDER_DEFAULT = 0;
- public static final int NAME_ORDER_EUROPE = 0x4;
- public static final int NAME_ORDER_JAPANESE = 0x8;
- private static final int NAME_ORDER_MASK = 0xC;
-
- // 0x10 is reserved for safety
-
- private static final int FLAG_CHARSET_UTF8 = 0;
- private static final int FLAG_CHARSET_SHIFT_JIS = 0x100;
- private static final int FLAG_CHARSET_MASK = 0xF00;
-
- /**
- * The flag indicating the vCard composer will add some "X-" properties used only in Android
- * when the formal vCard specification does not have appropriate fields for that data.
- *
- * For example, Android accepts nickname information while vCard 2.1 does not.
- * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME")
- * instead of just dropping it.
- *
- * vCard parser code automatically parses the field emitted even when this flag is off.
- *
- * Note that this flag does not assure all the information must be hold in the emitted vCard.
- */
- private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000;
-
- /**
- * The flag indicating the vCard composer will add some "X-" properties seen in the
- * vCard data emitted by the other softwares/devices when the formal vCard specification
- * does not have appropriate field(s) for that data.
- *
- * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are
- * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other
- * non-Android devices/softwares. We chose to enable the vCard composer to use those
- * defact properties since they are also useful for Android devices.
- *
- * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0
- * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens
- * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties.
- */
- private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000;
-
- /**
- * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese
- * mobile careers) should be used. This flag does not include any other information like
- * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's
- * dialect but the name order should be European", but it is not recommended.
- */
- private static final int FLAG_DOCOMO = 0x20000000;
-
- /**
- * <P>
- * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary"
- * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0).
- * </P>
- * <P>
- * We actually cannot define what is the "primary" property. Note that this is NOT defined
- * in vCard specification either. Also be aware that it is NOT related to "primary" notion
- * used in {@link android.provider.ContactsContract}.
- * This notion is just for vCard composition in Android.
- * </P>
- * <P>
- * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1
- * do NOT use Quoted-Printable encoding toward some properties related names like "N", "FN", etc.
- * even when their values contain non-ascii or/and CR/LF, while they use the encoding in the
- * other properties like "ADR", "ORG", etc.
- * <P>
- * We are afraid of the case where some vCard importer also forget handling QP presuming QP is
- * not used in such fields.
- * </P>
- * <P>
- * This flag is useful when some target importer you are going to focus on does not accept
- * such properties with Quoted-Printable encoding.
- * </P>
- * <P>
- * Again, we should not use this flag at all for complying vCard 2.1 spec.
- * </P>
- * <P>
- * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this
- * kind of problem (hopefully).
- * </P>
- */
- public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000;
-
- /**
- * <P>
- * The flag indicating that phonetic name related fields must be converted to
- * appropriate form. Note that "appropriate" is not defined in any vCard specification.
- * This is Android-specific.
- * </P>
- * <P>
- * One typical (and currently sole) example where we need this flag is the time when
- * we need to emit Japanese phonetic names into vCard entries. The property values
- * should be encoded into half-width katakana when the target importer is Japanese mobile
- * phones', which are probably not able to parse full-width hiragana/katakana for
- * historical reasons, while the vCard importers embedded to softwares for PC should be
- * able to parse them as we expect.
- * </P>
- */
- public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000;
-
- /**
- * <P>
- * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string toward TYPE params
- * every time possible. The default behavior does not emit it and is valid in the spec.
- * In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in vCard 3.0 specification.
- * </P>
- * <P>
- * Detail:
- * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0.
- * </p>
- * <P>
- * e.g.<BR />
- * 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."<BR />
- * 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."<BR />
- * 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."<BR />
- * </P>
- * <P>
- * 2) had been the default of VCard exporter/importer in Android, but it is found that
- * some external exporter is not able to parse the type format like 2) but only 3).
- * </P>
- * <P>
- * If you are targeting to the importer which cannot accept TYPE params without "TYPE="
- * strings (which should be rare though), please use this flag.
- * </P>
- * <P>
- * Example usage: int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM);
- * </P>
- */
- public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000;
-
- /**
- * <P>
- * The flag asking exporter to refrain image export.
- * </P>
- * @hide will be deleted in the near future.
- */
- public static final int FLAG_REFRAIN_IMAGE_EXPORT = 0x02000000;
-
- /**
- * <P>
- * The flag indicating the vCard composer does touch nothing toward phone number Strings
- * but leave it as is.
- * </P>
- * <P>
- * The vCard specifications mention nothing toward phone numbers, while some devices
- * do (wrongly, but with innevitable reasons).
- * For example, there's a possibility Japanese mobile phones are expected to have
- * just numbers, hypens, plus, etc. but not usual alphabets, while US mobile phones
- * should get such characters. To make exported vCard simple for external parsers,
- * we have used {@link PhoneNumberUtils#formatNumber(String)} during export, and
- * removed unnecessary characters inside the number (e.g. "111-222-3333 (Miami)"
- * becomes "111-222-3333").
- * Unfortunate side effect of that use was some control characters used in the other
- * areas may be badly affected by the formatting.
- * </P>
- * <P>
- * This flag disables that formatting, affecting both importer and exporter.
- * If the user is aware of some side effects due to the implicit formatting, use this flag.
- * </P>
- */
- public static final int FLAG_REFRAIN_PHONE_NUMBER_FORMATTING = 0x02000000;
-
- //// The followings are VCard types available from importer/exporter. ////
-
- /**
- * <P>
- * Generic vCard format with the vCard 2.1. Uses UTF-8 for the charset.
- * When composing a vCard entry, the US convension will be used toward formatting
- * some values.
- * </P>
- * <P>
- * e.g. The order of the display name would be "Prefix Given Middle Family Suffix",
- * while it should be "Prefix Family Middle Given Suffix" in Japan for example.
- * </P>
- */
- public static final int VCARD_TYPE_V21_GENERIC_UTF8 =
- (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic";
-
- /**
- * <P>
- * General vCard format with the version 3.0. Uses UTF-8 for the charset.
- * </P>
- * <P>
- * Not fully ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V30_GENERIC_UTF8 =
- (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic";
-
- /**
- * <P>
- * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8.
- * Currently, only name order is considered ("Prefix Middle Given Family Suffix")
- * </P>
- */
- public static final int VCARD_TYPE_V21_EUROPE_UTF8 =
- (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe";
-
- /**
- * <P>
- * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8.
- * </P>
- * <P>
- * Not ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V30_EUROPE_UTF8 =
- (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe";
-
- /**
- * <P>
- * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset.
- * </P>
- * <P>
- * Not ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V21_JAPANESE_UTF8 =
- (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8";
-
- /**
- * <P>
- * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for
- * parsing/composing the vCard data.
- * </P>
- * <P>
- * Not ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V21_JAPANESE_SJIS =
- (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis";
-
- /**
- * <P>
- * vCard format for miscellaneous Japanese devices, using Shift_Jis for
- * parsing/composing the vCard data.
- * </P>
- * <P>
- * Not ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V30_JAPANESE_SJIS =
- (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis";
-
- /**
- * <P>
- * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset.
- * </P>
- * <P>
- * Not ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V30_JAPANESE_UTF8 =
- (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 |
- FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY);
-
- /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8";
-
- /**
- * <P>
- * The vCard 2.1 based format which (partially) considers the convention in Japanese
- * mobile phones, where phonetic names are translated to half-width katakana if
- * possible, etc.
- * </P>
- * <P>
- * Not ready yet. Use with caution when you use this.
- * </P>
- */
- public static final int VCARD_TYPE_V21_JAPANESE_MOBILE =
- (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS |
- FLAG_CONVERT_PHONETIC_NAME_STRINGS |
- FLAG_REFRAIN_QP_TO_NAME_PROPERTIES);
-
- /* package */ static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile";
-
- /**
- * <P>
- * VCard format used in DoCoMo, which is one of Japanese mobile phone careers.
- * </p>
- * <P>
- * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions.
- * No Android-specific property nor defact property is included. The "Primary" properties
- * are NOT encoded to Quoted-Printable.
- * </P>
- */
- public static final int VCARD_TYPE_DOCOMO =
- (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO);
-
- /* package */ static final String VCARD_TYPE_DOCOMO_STR = "docomo";
-
- public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8;
-
- private static final Map<String, Integer> sVCardTypeMap;
- private static final Set<Integer> sJapaneseMobileTypeSet;
-
- static {
- sVCardTypeMap = new HashMap<String, Integer>();
- sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8);
- sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8);
- sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8);
- sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8);
- sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS);
- sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8);
- sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS);
- sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8);
- sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE);
- sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO);
-
- sJapaneseMobileTypeSet = new HashSet<Integer>();
- sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS);
- sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8);
- sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS);
- sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS);
- sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8);
- sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE);
- sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO);
- }
-
- public static int getVCardTypeFromString(final String vcardTypeString) {
- final String loweredKey = vcardTypeString.toLowerCase();
- if (sVCardTypeMap.containsKey(loweredKey)) {
- return sVCardTypeMap.get(loweredKey);
- } else if ("default".equalsIgnoreCase(vcardTypeString)) {
- return VCARD_TYPE_DEFAULT;
- } else {
- Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\"");
- return VCARD_TYPE_DEFAULT;
- }
- }
-
- public static boolean isV30(final int vcardType) {
- return ((vcardType & FLAG_V30) != 0);
- }
-
- public static boolean shouldUseQuotedPrintable(final int vcardType) {
- return !isV30(vcardType);
- }
-
- public static boolean usesUtf8(final int vcardType) {
- return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8);
- }
-
- public static boolean usesShiftJis(final int vcardType) {
- return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS);
- }
-
- public static int getNameOrderType(final int vcardType) {
- return vcardType & NAME_ORDER_MASK;
- }
-
- public static boolean usesAndroidSpecificProperty(final int vcardType) {
- return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0);
- }
-
- public static boolean usesDefactProperty(final int vcardType) {
- return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0);
- }
-
- public static boolean showPerformanceLog() {
- return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0;
- }
-
- public static boolean shouldRefrainQPToNameProperties(final int vcardType) {
- return (!shouldUseQuotedPrintable(vcardType) ||
- ((vcardType & FLAG_REFRAIN_QP_TO_NAME_PROPERTIES) != 0));
- }
-
- public static boolean appendTypeParamName(final int vcardType) {
- return (isV30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0));
- }
-
- /**
- * @return true if the device is Japanese and some Japanese convension is
- * applied to creating "formatted" something like FORMATTED_ADDRESS.
- */
- public static boolean isJapaneseDevice(final int vcardType) {
- // TODO: Some mask will be required so that this method wrongly interpret
- // Japanese"-like" vCard type.
- // e.g. VCARD_TYPE_V21_JAPANESE_SJIS | FLAG_APPEND_TYPE_PARAMS
- return sJapaneseMobileTypeSet.contains(vcardType);
- }
-
- /* package */ static boolean refrainPhoneNumberFormatting(final int vcardType) {
- return ((vcardType & FLAG_REFRAIN_PHONE_NUMBER_FORMATTING) != 0);
- }
-
- public static boolean needsToConvertPhoneticString(final int vcardType) {
- return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0);
- }
-
- public static boolean onlyOneNoteFieldIsAvailable(final int vcardType) {
- return vcardType == VCARD_TYPE_DOCOMO;
- }
-
- public static boolean isDoCoMo(final int vcardType) {
- return ((vcardType & FLAG_DOCOMO) != 0);
- }
-
- private VCardConfig() {
- }
-}
diff --git a/core/java/android/pim/vcard/VCardConstants.java b/core/java/android/pim/vcard/VCardConstants.java
deleted file mode 100644
index 8c07126..0000000
--- a/core/java/android/pim/vcard/VCardConstants.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-/**
- * Constants used in both exporter and importer code.
- */
-public class VCardConstants {
- public static final String VERSION_V21 = "2.1";
- public static final String VERSION_V30 = "3.0";
-
- // The property names valid both in vCard 2.1 and 3.0.
- public static final String PROPERTY_BEGIN = "BEGIN";
- public static final String PROPERTY_VERSION = "VERSION";
- public static final String PROPERTY_N = "N";
- public static final String PROPERTY_FN = "FN";
- public static final String PROPERTY_ADR = "ADR";
- public static final String PROPERTY_EMAIL = "EMAIL";
- public static final String PROPERTY_NOTE = "NOTE";
- public static final String PROPERTY_ORG = "ORG";
- public static final String PROPERTY_SOUND = "SOUND"; // Not fully supported.
- public static final String PROPERTY_TEL = "TEL";
- public static final String PROPERTY_TITLE = "TITLE";
- public static final String PROPERTY_ROLE = "ROLE";
- public static final String PROPERTY_PHOTO = "PHOTO";
- public static final String PROPERTY_LOGO = "LOGO";
- public static final String PROPERTY_URL = "URL";
- public static final String PROPERTY_BDAY = "BDAY"; // Birthday
- public static final String PROPERTY_END = "END";
-
- // Valid property names not supported (not appropriately handled) by our vCard importer now.
- public static final String PROPERTY_REV = "REV";
- public static final String PROPERTY_AGENT = "AGENT";
-
- // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file.
- public static final String PROPERTY_NAME = "NAME";
- public static final String PROPERTY_NICKNAME = "NICKNAME";
- public static final String PROPERTY_SORT_STRING = "SORT-STRING";
-
- // De-fact property values expressing phonetic names.
- public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME";
- public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME";
- public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME";
-
- // Properties both ContactsStruct in Eclair and de-fact vCard extensions
- // shown in http://en.wikipedia.org/wiki/VCard support are defined here.
- public static final String PROPERTY_X_AIM = "X-AIM";
- public static final String PROPERTY_X_MSN = "X-MSN";
- public static final String PROPERTY_X_YAHOO = "X-YAHOO";
- public static final String PROPERTY_X_ICQ = "X-ICQ";
- public static final String PROPERTY_X_JABBER = "X-JABBER";
- public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK";
- public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME";
- // Properties only ContactsStruct has. We alse use this.
- public static final String PROPERTY_X_QQ = "X-QQ";
- public static final String PROPERTY_X_NETMEETING = "X-NETMEETING";
-
- // Phone number for Skype, available as usual phone.
- public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER";
-
- // Property for Android-specific fields.
- public static final String PROPERTY_X_ANDROID_CUSTOM = "X-ANDROID-CUSTOM";
-
- // Properties for DoCoMo vCard.
- public static final String PROPERTY_X_CLASS = "X-CLASS";
- public static final String PROPERTY_X_REDUCTION = "X-REDUCTION";
- public static final String PROPERTY_X_NO = "X-NO";
- public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE";
-
- public static final String PARAM_TYPE = "TYPE";
-
- public static final String PARAM_TYPE_HOME = "HOME";
- public static final String PARAM_TYPE_WORK = "WORK";
- public static final String PARAM_TYPE_FAX = "FAX";
- public static final String PARAM_TYPE_CELL = "CELL";
- public static final String PARAM_TYPE_VOICE = "VOICE";
- public static final String PARAM_TYPE_INTERNET = "INTERNET";
-
- // Abbreviation of "prefered" according to vCard 2.1 specification.
- // We interpret this value as "primary" property during import/export.
- //
- // Note: Both vCard specs does not mention anything about the requirement for this parameter,
- // but there may be some vCard importer which will get confused with more than
- // one "PREF"s in one property name, while Android accepts them.
- public static final String PARAM_TYPE_PREF = "PREF";
-
- // Phone type parameters valid in vCard and known to ContactsContract, but not so common.
- public static final String PARAM_TYPE_CAR = "CAR";
- public static final String PARAM_TYPE_ISDN = "ISDN";
- public static final String PARAM_TYPE_PAGER = "PAGER";
- public static final String PARAM_TYPE_TLX = "TLX"; // Telex
-
- // Phone types existing in vCard 2.1 but not known to ContactsContract.
- public static final String PARAM_TYPE_MODEM = "MODEM";
- public static final String PARAM_TYPE_MSG = "MSG";
- public static final String PARAM_TYPE_BBS = "BBS";
- public static final String PARAM_TYPE_VIDEO = "VIDEO";
-
- // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1).
- // These types are basically encoded to "X-" parameters when composing vCard.
- // Parser passes these when "X-" is added to the parameter or not.
- public static final String PARAM_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK";
- public static final String PARAM_PHONE_EXTRA_TYPE_RADIO = "RADIO";
- public static final String PARAM_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD";
- public static final String PARAM_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT";
- // vCard composer translates this type to "WORK" + "PREF". Just for parsing.
- public static final String PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN";
- // vCard composer translates this type to "VOICE" Just for parsing.
- public static final String PARAM_PHONE_EXTRA_TYPE_OTHER = "OTHER";
-
- // TYPE parameters for postal addresses.
- public static final String PARAM_ADR_TYPE_PARCEL = "PARCEL";
- public static final String PARAM_ADR_TYPE_DOM = "DOM";
- public static final String PARAM_ADR_TYPE_INTL = "INTL";
-
- // TYPE parameters not officially valid but used in some vCard exporter.
- // Do not use in composer side.
- public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY";
-
- // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of SORT-STRING in
- // vCard 3.0.
- public static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N";
-
- public interface ImportOnly {
- public static final String PROPERTY_X_NICKNAME = "X-NICKNAME";
- // Some device emits this "X-" parameter for expressing Google Talk,
- // which is specifically invalid but should be always properly accepted, and emitted
- // in some special case (for that device/application).
- public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK";
- }
-
- /* package */ static final int MAX_DATA_COLUMN = 15;
-
- /* package */ static final int MAX_CHARACTER_NUMS_QP = 76;
- static final int MAX_CHARACTER_NUMS_BASE64_V30 = 75;
-
- private VCardConstants() {
- }
-} \ No newline at end of file
diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java
deleted file mode 100644
index 7c7e9b8..0000000
--- a/core/java/android/pim/vcard/VCardEntry.java
+++ /dev/null
@@ -1,1447 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.accounts.Account;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Event;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-
-/**
- * This class bridges between data structure of Contact app and VCard data.
- */
-public class VCardEntry {
- private static final String LOG_TAG = "VCardEntry";
-
- private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
-
- private static final String ACCOUNT_TYPE_GOOGLE = "com.google";
- private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
-
- private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
-
- static {
- sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
- sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
- sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
- sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
- sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
- sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
- sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
- sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
- Im.PROTOCOL_GOOGLE_TALK);
- }
-
- static public class PhoneData {
- public final int type;
- public final String data;
- public final String label;
- // isPrimary is changable only when there's no appropriate one existing in
- // the original VCard.
- public boolean isPrimary;
- public PhoneData(int type, String data, String label, boolean isPrimary) {
- this.type = type;
- this.data = data;
- this.label = label;
- this.isPrimary = isPrimary;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof PhoneData)) {
- return false;
- }
- PhoneData phoneData = (PhoneData)obj;
- return (type == phoneData.type && data.equals(phoneData.data) &&
- label.equals(phoneData.label) && isPrimary == phoneData.isPrimary);
- }
-
- @Override
- public String toString() {
- return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
- type, data, label, isPrimary);
- }
- }
-
- static public class EmailData {
- public final int type;
- public final String data;
- // Used only when TYPE is TYPE_CUSTOM.
- public final String label;
- // isPrimary is changable only when there's no appropriate one existing in
- // the original VCard.
- public boolean isPrimary;
- public EmailData(int type, String data, String label, boolean isPrimary) {
- this.type = type;
- this.data = data;
- this.label = label;
- this.isPrimary = isPrimary;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof EmailData)) {
- return false;
- }
- EmailData emailData = (EmailData)obj;
- return (type == emailData.type && data.equals(emailData.data) &&
- label.equals(emailData.label) && isPrimary == emailData.isPrimary);
- }
-
- @Override
- public String toString() {
- return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
- type, data, label, isPrimary);
- }
- }
-
- static public class PostalData {
- // Determined by vCard spec.
- // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
- public static final int ADDR_MAX_DATA_SIZE = 7;
- private final String[] dataArray;
- public final String pobox;
- public final String extendedAddress;
- public final String street;
- public final String localty;
- public final String region;
- public final String postalCode;
- public final String country;
- public final int type;
- public final String label;
- public boolean isPrimary;
-
- public PostalData(final int type, final List<String> propValueList,
- final String label, boolean isPrimary) {
- this.type = type;
- dataArray = new String[ADDR_MAX_DATA_SIZE];
-
- int size = propValueList.size();
- if (size > ADDR_MAX_DATA_SIZE) {
- size = ADDR_MAX_DATA_SIZE;
- }
-
- // adr-value = 0*6(text-value ";") text-value
- // ; PO Box, Extended Address, Street, Locality, Region, Postal
- // ; Code, Country Name
- //
- // Use Iterator assuming List may be LinkedList, though actually it is
- // always ArrayList in the current implementation.
- int i = 0;
- for (String addressElement : propValueList) {
- dataArray[i] = addressElement;
- if (++i >= size) {
- break;
- }
- }
- while (i < ADDR_MAX_DATA_SIZE) {
- dataArray[i++] = null;
- }
-
- this.pobox = dataArray[0];
- this.extendedAddress = dataArray[1];
- this.street = dataArray[2];
- this.localty = dataArray[3];
- this.region = dataArray[4];
- this.postalCode = dataArray[5];
- this.country = dataArray[6];
- this.label = label;
- this.isPrimary = isPrimary;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof PostalData)) {
- return false;
- }
- final PostalData postalData = (PostalData)obj;
- return (Arrays.equals(dataArray, postalData.dataArray) &&
- (type == postalData.type &&
- (type == StructuredPostal.TYPE_CUSTOM ?
- (label == postalData.label) : true)) &&
- (isPrimary == postalData.isPrimary));
- }
-
- public String getFormattedAddress(final int vcardType) {
- StringBuilder builder = new StringBuilder();
- boolean empty = true;
- if (VCardConfig.isJapaneseDevice(vcardType)) {
- // In Japan, the order is reversed.
- for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
- String addressPart = dataArray[i];
- if (!TextUtils.isEmpty(addressPart)) {
- if (!empty) {
- builder.append(' ');
- } else {
- empty = false;
- }
- builder.append(addressPart);
- }
- }
- } else {
- for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
- String addressPart = dataArray[i];
- if (!TextUtils.isEmpty(addressPart)) {
- if (!empty) {
- builder.append(' ');
- } else {
- empty = false;
- }
- builder.append(addressPart);
- }
- }
- }
-
- return builder.toString().trim();
- }
-
- @Override
- public String toString() {
- return String.format("type: %d, label: %s, isPrimary: %s",
- type, label, isPrimary);
- }
- }
-
- static public class OrganizationData {
- public final int type;
- // non-final is Intentional: we may change the values since this info is separated into
- // two parts in vCard: "ORG" + "TITLE".
- public String companyName;
- public String departmentName;
- public String titleName;
- public boolean isPrimary;
-
- public OrganizationData(int type,
- String companyName,
- String departmentName,
- String titleName,
- boolean isPrimary) {
- this.type = type;
- this.companyName = companyName;
- this.departmentName = departmentName;
- this.titleName = titleName;
- this.isPrimary = isPrimary;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof OrganizationData)) {
- return false;
- }
- OrganizationData organization = (OrganizationData)obj;
- return (type == organization.type &&
- TextUtils.equals(companyName, organization.companyName) &&
- TextUtils.equals(departmentName, organization.departmentName) &&
- TextUtils.equals(titleName, organization.titleName) &&
- isPrimary == organization.isPrimary);
- }
-
- public String getFormattedString() {
- final StringBuilder builder = new StringBuilder();
- if (!TextUtils.isEmpty(companyName)) {
- builder.append(companyName);
- }
-
- if (!TextUtils.isEmpty(departmentName)) {
- if (builder.length() > 0) {
- builder.append(", ");
- }
- builder.append(departmentName);
- }
-
- if (!TextUtils.isEmpty(titleName)) {
- if (builder.length() > 0) {
- builder.append(", ");
- }
- builder.append(titleName);
- }
-
- return builder.toString();
- }
-
- @Override
- public String toString() {
- return String.format(
- "type: %d, company: %s, department: %s, title: %s, isPrimary: %s",
- type, companyName, departmentName, titleName, isPrimary);
- }
- }
-
- static public class ImData {
- public final int protocol;
- public final String customProtocol;
- public final int type;
- public final String data;
- public final boolean isPrimary;
-
- public ImData(final int protocol, final String customProtocol, final int type,
- final String data, final boolean isPrimary) {
- this.protocol = protocol;
- this.customProtocol = customProtocol;
- this.type = type;
- this.data = data;
- this.isPrimary = isPrimary;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof ImData)) {
- return false;
- }
- ImData imData = (ImData)obj;
- return (type == imData.type && protocol == imData.protocol
- && (customProtocol != null ? customProtocol.equals(imData.customProtocol) :
- (imData.customProtocol == null))
- && (data != null ? data.equals(imData.data) : (imData.data == null))
- && isPrimary == imData.isPrimary);
- }
-
- @Override
- public String toString() {
- return String.format(
- "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s",
- type, protocol, customProtocol, data, isPrimary);
- }
- }
-
- public static class PhotoData {
- public static final String FORMAT_FLASH = "SWF";
- public final int type;
- public final String formatName; // used when type is not defined in ContactsContract.
- public final byte[] photoBytes;
- public final boolean isPrimary;
-
- public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) {
- this.type = type;
- this.formatName = formatName;
- this.photoBytes = photoBytes;
- this.isPrimary = isPrimary;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof PhotoData)) {
- return false;
- }
- PhotoData photoData = (PhotoData)obj;
- return (type == photoData.type &&
- (formatName == null ? (photoData.formatName == null) :
- formatName.equals(photoData.formatName)) &&
- (Arrays.equals(photoBytes, photoData.photoBytes)) &&
- (isPrimary == photoData.isPrimary));
- }
-
- @Override
- public String toString() {
- return String.format("type: %d, format: %s: size: %d, isPrimary: %s",
- type, formatName, photoBytes.length, isPrimary);
- }
- }
-
- /* package */ static class Property {
- private String mPropertyName;
- private Map<String, Collection<String>> mParameterMap =
- new HashMap<String, Collection<String>>();
- private List<String> mPropertyValueList = new ArrayList<String>();
- private byte[] mPropertyBytes;
-
- public void setPropertyName(final String propertyName) {
- mPropertyName = propertyName;
- }
-
- public void addParameter(final String paramName, final String paramValue) {
- Collection<String> values;
- if (!mParameterMap.containsKey(paramName)) {
- if (paramName.equals("TYPE")) {
- values = new HashSet<String>();
- } else {
- values = new ArrayList<String>();
- }
- mParameterMap.put(paramName, values);
- } else {
- values = mParameterMap.get(paramName);
- }
- values.add(paramValue);
- }
-
- public void addToPropertyValueList(final String propertyValue) {
- mPropertyValueList.add(propertyValue);
- }
-
- public void setPropertyBytes(final byte[] propertyBytes) {
- mPropertyBytes = propertyBytes;
- }
-
- public final Collection<String> getParameters(String type) {
- return mParameterMap.get(type);
- }
-
- public final List<String> getPropertyValueList() {
- return mPropertyValueList;
- }
-
- public void clear() {
- mPropertyName = null;
- mParameterMap.clear();
- mPropertyValueList.clear();
- mPropertyBytes = null;
- }
- }
-
- private String mFamilyName;
- private String mGivenName;
- private String mMiddleName;
- private String mPrefix;
- private String mSuffix;
-
- // Used only when no family nor given name is found.
- private String mFullName;
-
- private String mPhoneticFamilyName;
- private String mPhoneticGivenName;
- private String mPhoneticMiddleName;
-
- private String mPhoneticFullName;
-
- private List<String> mNickNameList;
-
- private String mDisplayName;
-
- private String mBirthday;
-
- private List<String> mNoteList;
- private List<PhoneData> mPhoneList;
- private List<EmailData> mEmailList;
- private List<PostalData> mPostalList;
- private List<OrganizationData> mOrganizationList;
- private List<ImData> mImList;
- private List<PhotoData> mPhotoList;
- private List<String> mWebsiteList;
- private List<List<String>> mAndroidCustomPropertyList;
-
- private final int mVCardType;
- private final Account mAccount;
-
- public VCardEntry() {
- this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8);
- }
-
- public VCardEntry(int vcardType) {
- this(vcardType, null);
- }
-
- public VCardEntry(int vcardType, Account account) {
- mVCardType = vcardType;
- mAccount = account;
- }
-
- private void addPhone(int type, String data, String label, boolean isPrimary) {
- if (mPhoneList == null) {
- mPhoneList = new ArrayList<PhoneData>();
- }
- final StringBuilder builder = new StringBuilder();
- final String trimed = data.trim();
- final String formattedNumber;
- if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
- formattedNumber = trimed;
- } else {
- final 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);
- }
- }
-
- // Use NANP in default when there's no information about locale.
- final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
- formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType);
- }
- PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary);
- mPhoneList.add(phoneData);
- }
-
- private void addNickName(final String nickName) {
- if (mNickNameList == null) {
- mNickNameList = new ArrayList<String>();
- }
- mNickNameList.add(nickName);
- }
-
- private void addEmail(int type, String data, String label, boolean isPrimary){
- if (mEmailList == null) {
- mEmailList = new ArrayList<EmailData>();
- }
- mEmailList.add(new EmailData(type, data, label, isPrimary));
- }
-
- private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){
- if (mPostalList == null) {
- mPostalList = new ArrayList<PostalData>(0);
- }
- mPostalList.add(new PostalData(type, propValueList, label, isPrimary));
- }
-
- /**
- * Should be called via {@link #handleOrgValue(int, List, boolean)} or
- * {@link #handleTitleValue(String)}.
- */
- private void addNewOrganization(int type, final String companyName,
- final String departmentName,
- final String titleName, boolean isPrimary) {
- if (mOrganizationList == null) {
- mOrganizationList = new ArrayList<OrganizationData>();
- }
- mOrganizationList.add(new OrganizationData(type, companyName,
- departmentName, titleName, isPrimary));
- }
-
- private static final List<String> sEmptyList =
- Collections.unmodifiableList(new ArrayList<String>(0));
-
- /**
- * Set "ORG" related values to the appropriate data. If there's more than one
- * {@link OrganizationData} objects, this input data are attached to the last one which
- * does not have valid values (not including empty but only null). If there's no
- * {@link OrganizationData} object, a new {@link OrganizationData} is created,
- * whose title is set to null.
- */
- private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) {
- if (orgList == null) {
- orgList = sEmptyList;
- }
- final String companyName;
- final String departmentName;
- final int size = orgList.size();
- switch (size) {
- case 0: {
- companyName = "";
- departmentName = null;
- break;
- }
- case 1: {
- companyName = orgList.get(0);
- departmentName = null;
- break;
- }
- default: { // More than 1.
- companyName = orgList.get(0);
- // We're not sure which is the correct string for department.
- // In order to keep all the data, concatinate the rest of elements.
- StringBuilder builder = new StringBuilder();
- for (int i = 1; i < size; i++) {
- if (i > 1) {
- builder.append(' ');
- }
- builder.append(orgList.get(i));
- }
- departmentName = builder.toString();
- }
- }
- if (mOrganizationList == null) {
- // Create new first organization entry, with "null" title which may be
- // added via handleTitleValue().
- addNewOrganization(type, companyName, departmentName, null, isPrimary);
- return;
- }
- for (OrganizationData organizationData : mOrganizationList) {
- // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
- // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
- if (organizationData.companyName == null &&
- organizationData.departmentName == null) {
- // Probably the "TITLE" property comes before the "ORG" property via
- // handleTitleLine().
- organizationData.companyName = companyName;
- organizationData.departmentName = departmentName;
- organizationData.isPrimary = isPrimary;
- return;
- }
- }
- // No OrganizatioData is available. Create another one, with "null" title, which may be
- // added via handleTitleValue().
- addNewOrganization(type, companyName, departmentName, null, isPrimary);
- }
-
- /**
- * Set "title" value to the appropriate data. If there's more than one
- * OrganizationData objects, this input is attached to the last one which does not
- * have valid title value (not including empty but only null). If there's no
- * OrganizationData object, a new OrganizationData is created, whose company name is
- * set to null.
- */
- private void handleTitleValue(final String title) {
- if (mOrganizationList == null) {
- // Create new first organization entry, with "null" other info, which may be
- // added via handleOrgValue().
- addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
- return;
- }
- for (OrganizationData organizationData : mOrganizationList) {
- if (organizationData.titleName == null) {
- organizationData.titleName = title;
- return;
- }
- }
- // No Organization is available. Create another one, with "null" other info, which may be
- // added via handleOrgValue().
- addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
- }
-
- private void addIm(int protocol, String customProtocol, int type,
- String propValue, boolean isPrimary) {
- if (mImList == null) {
- mImList = new ArrayList<ImData>();
- }
- mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary));
- }
-
- private void addNote(final String note) {
- if (mNoteList == null) {
- mNoteList = new ArrayList<String>(1);
- }
- mNoteList.add(note);
- }
-
- private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
- if (mPhotoList == null) {
- mPhotoList = new ArrayList<PhotoData>(1);
- }
- final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary);
- mPhotoList.add(photoData);
- }
-
- @SuppressWarnings("fallthrough")
- private void handleNProperty(List<String> elems) {
- // Family, Given, Middle, Prefix, Suffix. (1 - 5)
- int size;
- if (elems == null || (size = elems.size()) < 1) {
- return;
- }
- if (size > 5) {
- size = 5;
- }
-
- switch (size) {
- // fallthrough
- case 5: mSuffix = elems.get(4);
- case 4: mPrefix = elems.get(3);
- case 3: mMiddleName = elems.get(2);
- case 2: mGivenName = elems.get(1);
- default: mFamilyName = elems.get(0);
- }
- }
-
- /**
- * Note: 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 ';'s in it.
- * Assume the ';' means the same meaning in N property
- */
- @SuppressWarnings("fallthrough")
- private void handlePhoneticNameFromSound(List<String> elems) {
- if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
- TextUtils.isEmpty(mPhoneticMiddleName) &&
- TextUtils.isEmpty(mPhoneticGivenName))) {
- // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
- // Ignore "SOUND;X-IRMC-N".
- return;
- }
-
- int size;
- if (elems == null || (size = elems.size()) < 1) {
- return;
- }
-
- // Assume that the order is "Family, Given, Middle".
- // This is not from specification but mere assumption. Some Japanese phones use this order.
- if (size > 3) {
- size = 3;
- }
-
- if (elems.get(0).length() > 0) {
- boolean onlyFirstElemIsNonEmpty = true;
- for (int i = 1; i < size; i++) {
- if (elems.get(i).length() > 0) {
- onlyFirstElemIsNonEmpty = false;
- break;
- }
- }
- if (onlyFirstElemIsNonEmpty) {
- final String[] namesArray = elems.get(0).split(" ");
- final int nameArrayLength = namesArray.length;
- if (nameArrayLength == 3) {
- // Assume the string is "Family Middle Given".
- mPhoneticFamilyName = namesArray[0];
- mPhoneticMiddleName = namesArray[1];
- mPhoneticGivenName = namesArray[2];
- } else if (nameArrayLength == 2) {
- // Assume the string is "Family Given" based on the Japanese mobile
- // phones' preference.
- mPhoneticFamilyName = namesArray[0];
- mPhoneticGivenName = namesArray[1];
- } else {
- mPhoneticFullName = elems.get(0);
- }
- return;
- }
- }
-
- switch (size) {
- // fallthrough
- case 3: mPhoneticMiddleName = elems.get(2);
- case 2: mPhoneticGivenName = elems.get(1);
- default: mPhoneticFamilyName = elems.get(0);
- }
- }
-
- public void addProperty(final Property property) {
- final String propName = property.mPropertyName;
- final Map<String, Collection<String>> paramMap = property.mParameterMap;
- final List<String> propValueList = property.mPropertyValueList;
- byte[] propBytes = property.mPropertyBytes;
-
- if (propValueList.size() == 0) {
- return;
- }
- final String propValue = listToString(propValueList).trim();
-
- if (propName.equals(VCardConstants.PROPERTY_VERSION)) {
- // vCard version. Ignore this.
- } else if (propName.equals(VCardConstants.PROPERTY_FN)) {
- mFullName = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) {
- // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not
- // actually exist in the real vCard data, does not exist.
- mFullName = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_N)) {
- handleNProperty(propValueList);
- } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
- mPhoneticFullName = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) ||
- propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
- addNickName(propValue);
- } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) {
- Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- if (typeCollection != null
- && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
- // As of 2009-10-08, Parser side does not split a property value into separated
- // values using ';' (in other words, propValueList.size() == 1),
- // which is correct behavior from the view of vCard 2.1.
- // But we want it to be separated, so do the separation here.
- final List<String> phoneticNameList =
- VCardUtils.constructListFromValue(propValue,
- VCardConfig.isV30(mVCardType));
- handlePhoneticNameFromSound(phoneticNameList);
- } else {
- // Ignore this field since Android cannot understand what it is.
- }
- } else if (propName.equals(VCardConstants.PROPERTY_ADR)) {
- boolean valuesAreAllEmpty = true;
- for (String value : propValueList) {
- if (value.length() > 0) {
- valuesAreAllEmpty = false;
- break;
- }
- }
- if (valuesAreAllEmpty) {
- return;
- }
-
- int type = -1;
- String label = "";
- boolean isPrimary = false;
- Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- if (typeCollection != null) {
- for (String typeString : typeCollection) {
- typeString = typeString.toUpperCase();
- if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
- isPrimary = true;
- } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
- type = StructuredPostal.TYPE_HOME;
- label = "";
- } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) ||
- typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
- // "COMPANY" seems emitted by Windows Mobile, which is not
- // specifically supported by vCard 2.1. We assume this is same
- // as "WORK".
- type = StructuredPostal.TYPE_WORK;
- label = "";
- } else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) ||
- typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) ||
- typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
- // We do not have any appropriate way to store this information.
- } else {
- if (typeString.startsWith("X-") && type < 0) {
- typeString = typeString.substring(2);
- }
- // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters
- // emit non-standard types. We do not handle their values now.
- type = StructuredPostal.TYPE_CUSTOM;
- label = typeString;
- }
- }
- }
- // We use "HOME" as default
- if (type < 0) {
- type = StructuredPostal.TYPE_HOME;
- }
-
- addPostal(type, propValueList, label, isPrimary);
- } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) {
- int type = -1;
- String label = null;
- boolean isPrimary = false;
- Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- if (typeCollection != null) {
- for (String typeString : typeCollection) {
- typeString = typeString.toUpperCase();
- if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
- isPrimary = true;
- } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
- type = Email.TYPE_HOME;
- } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) {
- type = Email.TYPE_WORK;
- } else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) {
- type = Email.TYPE_MOBILE;
- } else {
- if (typeString.startsWith("X-") && type < 0) {
- typeString = typeString.substring(2);
- }
- // vCard 3.0 allows iana-token.
- // We may have INTERNET (specified in vCard spec),
- // SCHOOL, etc.
- type = Email.TYPE_CUSTOM;
- label = typeString;
- }
- }
- }
- if (type < 0) {
- type = Email.TYPE_OTHER;
- }
- addEmail(type, propValue, label, isPrimary);
- } else if (propName.equals(VCardConstants.PROPERTY_ORG)) {
- // vCard specification does not specify other types.
- final int type = Organization.TYPE_WORK;
- boolean isPrimary = false;
- Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- if (typeCollection != null) {
- for (String typeString : typeCollection) {
- if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
- isPrimary = true;
- }
- }
- }
- handleOrgValue(type, propValueList, isPrimary);
- } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) {
- handleTitleValue(propValue);
- } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) {
- // This conflicts with TITLE. Ignore for now...
- // handleTitleValue(propValue);
- } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) ||
- propName.equals(VCardConstants.PROPERTY_LOGO)) {
- Collection<String> paramMapValue = paramMap.get("VALUE");
- if (paramMapValue != null && paramMapValue.contains("URL")) {
- // Currently we do not have appropriate example for testing this case.
- } else {
- final Collection<String> typeCollection = paramMap.get("TYPE");
- String formatName = null;
- boolean isPrimary = false;
- if (typeCollection != null) {
- for (String typeValue : typeCollection) {
- if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
- isPrimary = true;
- } else if (formatName == null){
- formatName = typeValue;
- }
- }
- }
- addPhotoBytes(formatName, propBytes, isPrimary);
- }
- } else if (propName.equals(VCardConstants.PROPERTY_TEL)) {
- final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- final Object typeObject =
- VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue);
- final int type;
- final String label;
- if (typeObject instanceof Integer) {
- type = (Integer)typeObject;
- label = null;
- } else {
- type = Phone.TYPE_CUSTOM;
- label = typeObject.toString();
- }
-
- final boolean isPrimary;
- if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
- isPrimary = true;
- } else {
- isPrimary = false;
- }
- addPhone(type, propValue, label, isPrimary);
- } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
- // The phone number available via Skype.
- Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- final int type = Phone.TYPE_OTHER;
- final boolean isPrimary;
- if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
- isPrimary = true;
- } else {
- isPrimary = false;
- }
- addPhone(type, propValue, null, isPrimary);
- } else if (sImMap.containsKey(propName)) {
- final int protocol = sImMap.get(propName);
- boolean isPrimary = false;
- int type = -1;
- final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
- if (typeCollection != null) {
- for (String typeString : typeCollection) {
- if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
- isPrimary = true;
- } else if (type < 0) {
- if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
- type = Im.TYPE_HOME;
- } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
- type = Im.TYPE_WORK;
- }
- }
- }
- }
- if (type < 0) {
- type = Phone.TYPE_HOME;
- }
- addIm(protocol, null, type, propValue, isPrimary);
- } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) {
- addNote(propValue);
- } else if (propName.equals(VCardConstants.PROPERTY_URL)) {
- if (mWebsiteList == null) {
- mWebsiteList = new ArrayList<String>(1);
- }
- mWebsiteList.add(propValue);
- } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) {
- mBirthday = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
- mPhoneticGivenName = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
- mPhoneticMiddleName = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
- mPhoneticFamilyName = propValue;
- } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
- final List<String> customPropertyList =
- VCardUtils.constructListFromValue(propValue,
- VCardConfig.isV30(mVCardType));
- handleAndroidCustomProperty(customPropertyList);
- /*} else if (propName.equals("REV")) {
- // Revision of this VCard entry. I think we can ignore this.
- } else if (propName.equals("UID")) {
- } else if (propName.equals("KEY")) {
- // Type is X509 or PGP? I don't know how to handle this...
- } else if (propName.equals("MAILER")) {
- } else if (propName.equals("TZ")) {
- } else if (propName.equals("GEO")) {
- } else if (propName.equals("CLASS")) {
- // vCard 3.0 only.
- // e.g. CLASS:CONFIDENTIAL
- } else if (propName.equals("PROFILE")) {
- // VCard 3.0 only. Must be "VCARD". I think we can ignore this.
- } else if (propName.equals("CATEGORIES")) {
- // VCard 3.0 only.
- // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY
- } else if (propName.equals("SOURCE")) {
- // VCard 3.0 only.
- } else if (propName.equals("PRODID")) {
- // VCard 3.0 only.
- // To specify the identifier for the product that created
- // the vCard object.*/
- } else {
- // Unknown X- words and IANA token.
- }
- }
-
- private void handleAndroidCustomProperty(final List<String> customPropertyList) {
- if (mAndroidCustomPropertyList == null) {
- mAndroidCustomPropertyList = new ArrayList<List<String>>();
- }
- mAndroidCustomPropertyList.add(customPropertyList);
- }
-
- /**
- * Construct the display name. The constructed data must not be null.
- */
- private void constructDisplayName() {
- // FullName (created via "FN" or "NAME" field) is prefered.
- if (!TextUtils.isEmpty(mFullName)) {
- mDisplayName = mFullName;
- } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) {
- mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
- mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix);
- } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
- TextUtils.isEmpty(mPhoneticGivenName))) {
- mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
- mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName);
- } else if (mEmailList != null && mEmailList.size() > 0) {
- mDisplayName = mEmailList.get(0).data;
- } else if (mPhoneList != null && mPhoneList.size() > 0) {
- mDisplayName = mPhoneList.get(0).data;
- } else if (mPostalList != null && mPostalList.size() > 0) {
- mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType);
- } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
- mDisplayName = mOrganizationList.get(0).getFormattedString();
- }
-
- if (mDisplayName == null) {
- mDisplayName = "";
- }
- }
-
- /**
- * Consolidate several fielsds (like mName) using name candidates,
- */
- public void consolidateFields() {
- constructDisplayName();
-
- if (mPhoneticFullName != null) {
- mPhoneticFullName = mPhoneticFullName.trim();
- }
- }
-
- public Uri pushIntoContentResolver(ContentResolver resolver) {
- ArrayList<ContentProviderOperation> operationList =
- new ArrayList<ContentProviderOperation>();
- // After applying the batch the first result's Uri is returned so it is important that
- // the RawContact is the first operation that gets inserted into the list
- ContentProviderOperation.Builder builder =
- ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
- String myGroupsId = null;
- if (mAccount != null) {
- builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
- builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
-
- // Assume that caller side creates this group if it does not exist.
- if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) {
- final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] {
- Groups.SOURCE_ID },
- Groups.TITLE + "=?", new String[] {
- GOOGLE_MY_CONTACTS_GROUP }, null);
- try {
- if (cursor != null && cursor.moveToFirst()) {
- myGroupsId = cursor.getString(0);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- } else {
- builder.withValue(RawContacts.ACCOUNT_NAME, null);
- builder.withValue(RawContacts.ACCOUNT_TYPE, null);
- }
- operationList.add(builder.build());
-
- if (!nameFieldsAreEmpty()) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
-
- builder.withValue(StructuredName.GIVEN_NAME, mGivenName);
- builder.withValue(StructuredName.FAMILY_NAME, mFamilyName);
- builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName);
- builder.withValue(StructuredName.PREFIX, mPrefix);
- builder.withValue(StructuredName.SUFFIX, mSuffix);
-
- if (!(TextUtils.isEmpty(mPhoneticGivenName)
- && TextUtils.isEmpty(mPhoneticFamilyName)
- && TextUtils.isEmpty(mPhoneticMiddleName))) {
- builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName);
- builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName);
- builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName);
- } else if (!TextUtils.isEmpty(mPhoneticFullName)) {
- builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName);
- }
-
- builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
- operationList.add(builder.build());
- }
-
- if (mNickNameList != null && mNickNameList.size() > 0) {
- for (String nickName : mNickNameList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
- builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
- builder.withValue(Nickname.NAME, nickName);
- operationList.add(builder.build());
- }
- }
-
- if (mPhoneList != null) {
- for (PhoneData phoneData : mPhoneList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
-
- builder.withValue(Phone.TYPE, phoneData.type);
- if (phoneData.type == Phone.TYPE_CUSTOM) {
- builder.withValue(Phone.LABEL, phoneData.label);
- }
- builder.withValue(Phone.NUMBER, phoneData.data);
- if (phoneData.isPrimary) {
- builder.withValue(Phone.IS_PRIMARY, 1);
- }
- operationList.add(builder.build());
- }
- }
-
- if (mOrganizationList != null) {
- for (OrganizationData organizationData : mOrganizationList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
- builder.withValue(Organization.TYPE, organizationData.type);
- if (organizationData.companyName != null) {
- builder.withValue(Organization.COMPANY, organizationData.companyName);
- }
- if (organizationData.departmentName != null) {
- builder.withValue(Organization.DEPARTMENT, organizationData.departmentName);
- }
- if (organizationData.titleName != null) {
- builder.withValue(Organization.TITLE, organizationData.titleName);
- }
- if (organizationData.isPrimary) {
- builder.withValue(Organization.IS_PRIMARY, 1);
- }
- operationList.add(builder.build());
- }
- }
-
- if (mEmailList != null) {
- for (EmailData emailData : mEmailList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
-
- builder.withValue(Email.TYPE, emailData.type);
- if (emailData.type == Email.TYPE_CUSTOM) {
- builder.withValue(Email.LABEL, emailData.label);
- }
- builder.withValue(Email.DATA, emailData.data);
- if (emailData.isPrimary) {
- builder.withValue(Data.IS_PRIMARY, 1);
- }
- operationList.add(builder.build());
- }
- }
-
- if (mPostalList != null) {
- for (PostalData postalData : mPostalList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- VCardUtils.insertStructuredPostalDataUsingContactsStruct(
- mVCardType, builder, postalData);
- operationList.add(builder.build());
- }
- }
-
- if (mImList != null) {
- for (ImData imData : mImList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Im.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
- builder.withValue(Im.TYPE, imData.type);
- builder.withValue(Im.PROTOCOL, imData.protocol);
- if (imData.protocol == Im.PROTOCOL_CUSTOM) {
- builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol);
- }
- if (imData.isPrimary) {
- builder.withValue(Data.IS_PRIMARY, 1);
- }
- }
- }
-
- if (mNoteList != null) {
- for (String note : mNoteList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Note.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
- builder.withValue(Note.NOTE, note);
- operationList.add(builder.build());
- }
- }
-
- if (mPhotoList != null) {
- for (PhotoData photoData : mPhotoList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
- builder.withValue(Photo.PHOTO, photoData.photoBytes);
- if (photoData.isPrimary) {
- builder.withValue(Photo.IS_PRIMARY, 1);
- }
- operationList.add(builder.build());
- }
- }
-
- if (mWebsiteList != null) {
- for (String website : mWebsiteList) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Website.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
- builder.withValue(Website.URL, website);
- // There's no information about the type of URL in vCard.
- // We use TYPE_HOMEPAGE for safety.
- builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
- operationList.add(builder.build());
- }
- }
-
- if (!TextUtils.isEmpty(mBirthday)) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
- builder.withValue(Event.START_DATE, mBirthday);
- builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
- operationList.add(builder.build());
- }
-
- if (mAndroidCustomPropertyList != null) {
- for (List<String> customPropertyList : mAndroidCustomPropertyList) {
- int size = customPropertyList.size();
- if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) {
- continue;
- } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) {
- size = VCardConstants.MAX_DATA_COLUMN + 1;
- customPropertyList =
- customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2);
- }
-
- int i = 0;
- for (final String customPropertyValue : customPropertyList) {
- if (i == 0) {
- final String mimeType = customPropertyValue;
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, mimeType);
- } else { // 1 <= i && i <= MAX_DATA_COLUMNS
- if (!TextUtils.isEmpty(customPropertyValue)) {
- builder.withValue("data" + i, customPropertyValue);
- }
- }
-
- i++;
- }
- operationList.add(builder.build());
- }
- }
-
- if (myGroupsId != null) {
- builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
- builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
- builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId);
- operationList.add(builder.build());
- }
-
- try {
- ContentProviderResult[] results = resolver.applyBatch(
- ContactsContract.AUTHORITY, operationList);
- // the first result is always the raw_contact. return it's uri so
- // that it can be found later. do null checking for badly behaving
- // ContentResolvers
- return (results == null || results.length == 0 || results[0] == null)
- ? null
- : results[0].uri;
- } catch (RemoteException e) {
- Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
- return null;
- } catch (OperationApplicationException e) {
- Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
- return null;
- }
- }
-
- public static VCardEntry buildFromResolver(ContentResolver resolver) {
- return buildFromResolver(resolver, Contacts.CONTENT_URI);
- }
-
- public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
-
- return null;
- }
-
- private boolean nameFieldsAreEmpty() {
- return (TextUtils.isEmpty(mFamilyName)
- && TextUtils.isEmpty(mMiddleName)
- && TextUtils.isEmpty(mGivenName)
- && TextUtils.isEmpty(mPrefix)
- && TextUtils.isEmpty(mSuffix)
- && TextUtils.isEmpty(mFullName)
- && TextUtils.isEmpty(mPhoneticFamilyName)
- && TextUtils.isEmpty(mPhoneticMiddleName)
- && TextUtils.isEmpty(mPhoneticGivenName)
- && TextUtils.isEmpty(mPhoneticFullName));
- }
-
- public boolean isIgnorable() {
- return getDisplayName().length() == 0;
- }
-
- private String listToString(List<String> list){
- final 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 "";
- }
- }
-
- // All getter methods should be used carefully, since they may change
- // in the future as of 2009-10-05, on which I cannot be sure this structure
- // is completely consolidated.
- //
- // Also note that these getter methods should be used only after
- // all properties being pushed into this object. If not, incorrect
- // value will "be stored in the local cache and" be returned to you.
-
- public String getFamilyName() {
- return mFamilyName;
- }
-
- public String getGivenName() {
- return mGivenName;
- }
-
- public String getMiddleName() {
- return mMiddleName;
- }
-
- public String getPrefix() {
- return mPrefix;
- }
-
- public String getSuffix() {
- return mSuffix;
- }
-
- public String getFullName() {
- return mFullName;
- }
-
- public String getPhoneticFamilyName() {
- return mPhoneticFamilyName;
- }
-
- public String getPhoneticGivenName() {
- return mPhoneticGivenName;
- }
-
- public String getPhoneticMiddleName() {
- return mPhoneticMiddleName;
- }
-
- public String getPhoneticFullName() {
- return mPhoneticFullName;
- }
-
- public final List<String> getNickNameList() {
- return mNickNameList;
- }
-
- public String getBirthday() {
- return mBirthday;
- }
-
- public final List<String> getNotes() {
- return mNoteList;
- }
-
- public final List<PhoneData> getPhoneList() {
- return mPhoneList;
- }
-
- public final List<EmailData> getEmailList() {
- return mEmailList;
- }
-
- public final List<PostalData> getPostalList() {
- return mPostalList;
- }
-
- public final List<OrganizationData> getOrganizationList() {
- return mOrganizationList;
- }
-
- public final List<ImData> getImList() {
- return mImList;
- }
-
- public final List<PhotoData> getPhotoList() {
- return mPhotoList;
- }
-
- public final List<String> getWebsiteList() {
- return mWebsiteList;
- }
-
- public String getDisplayName() {
- if (mDisplayName == null) {
- constructDisplayName();
- }
- return mDisplayName;
- }
-}
diff --git a/core/java/android/pim/vcard/VCardEntryCommitter.java b/core/java/android/pim/vcard/VCardEntryCommitter.java
deleted file mode 100644
index 59a2baf..0000000
--- a/core/java/android/pim/vcard/VCardEntryCommitter.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.content.ContentResolver;
-import android.net.Uri;
-import android.util.Log;
-
-import java.util.ArrayList;
-
-/**
- * <P>
- * {@link VCardEntryHandler} implementation which commits the entry to ContentResolver.
- * </P>
- * <P>
- * Note:<BR />
- * Each vCard may contain big photo images encoded by BASE64,
- * If we store all vCard entries in memory, OutOfMemoryError may be thrown.
- * Thus, this class push each VCard entry into ContentResolver immediately.
- * </P>
- */
-public class VCardEntryCommitter implements VCardEntryHandler {
- public static String LOG_TAG = "VCardEntryComitter";
-
- private final ContentResolver mContentResolver;
- private long mTimeToCommit;
- private ArrayList<Uri> mCreatedUris = new ArrayList<Uri>();
-
- public VCardEntryCommitter(ContentResolver resolver) {
- mContentResolver = resolver;
- }
-
- public void onStart() {
- }
-
- public void onEnd() {
- if (VCardConfig.showPerformanceLog()) {
- Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit));
- }
- }
-
- public void onEntryCreated(final VCardEntry contactStruct) {
- long start = System.currentTimeMillis();
- mCreatedUris.add(contactStruct.pushIntoContentResolver(mContentResolver));
- mTimeToCommit += System.currentTimeMillis() - start;
- }
-
- /**
- * Returns the list of created Uris. This list should not be modified by the caller as it is
- * not a clone.
- */
- public ArrayList<Uri> getCreatedUris() {
- return mCreatedUris;
- }
-} \ No newline at end of file
diff --git a/core/java/android/pim/vcard/VCardEntryConstructor.java b/core/java/android/pim/vcard/VCardEntryConstructor.java
deleted file mode 100644
index 290ca2b..0000000
--- a/core/java/android/pim/vcard/VCardEntryConstructor.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.accounts.Account;
-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.Collection;
-import java.util.List;
-
-public class VCardEntryConstructor implements VCardInterpreter {
- private static String LOG_TAG = "VCardEntryConstructor";
-
- /**
- * If there's no other information available, this class uses this charset for encoding
- * byte arrays to String.
- */
- /* package */ static final String DEFAULT_CHARSET_FOR_DECODED_BYTES = "UTF-8";
-
- private VCardEntry.Property mCurrentProperty = new VCardEntry.Property();
- private VCardEntry mCurrentContactStruct;
- private String mParamType;
-
- /**
- * The charset using which {@link VCardInterpreter} parses the text.
- */
- private String mInputCharset;
-
- /**
- * The charset with which byte array is encoded to String.
- */
- final private String mCharsetForDecodedBytes;
- final private boolean mStrictLineBreakParsing;
- final private int mVCardType;
- final private Account mAccount;
-
- /** For measuring performance. */
- private long mTimePushIntoContentResolver;
-
- final private List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>();
-
- public VCardEntryConstructor() {
- this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null);
- }
-
- public VCardEntryConstructor(final int vcardType) {
- this(null, null, false, vcardType, null);
- }
-
- public VCardEntryConstructor(final String charset, final boolean strictLineBreakParsing,
- final int vcardType, final Account account) {
- this(null, charset, strictLineBreakParsing, vcardType, account);
- }
-
- public VCardEntryConstructor(final String inputCharset, final String charsetForDetodedBytes,
- final boolean strictLineBreakParsing, final int vcardType,
- final Account account) {
- if (inputCharset != null) {
- mInputCharset = inputCharset;
- } else {
- mInputCharset = VCardConfig.DEFAULT_CHARSET;
- }
- if (charsetForDetodedBytes != null) {
- mCharsetForDecodedBytes = charsetForDetodedBytes;
- } else {
- mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES;
- }
- mStrictLineBreakParsing = strictLineBreakParsing;
- mVCardType = vcardType;
- mAccount = account;
- }
-
- public void addEntryHandler(VCardEntryHandler entryHandler) {
- mEntryHandlers.add(entryHandler);
- }
-
- public void start() {
- for (VCardEntryHandler entryHandler : mEntryHandlers) {
- entryHandler.onStart();
- }
- }
-
- public void end() {
- for (VCardEntryHandler entryHandler : mEntryHandlers) {
- entryHandler.onEnd();
- }
- }
-
- /**
- * Called when the parse failed between {@link #startEntry()} and {@link #endEntry()}.
- */
- public void clear() {
- mCurrentContactStruct = null;
- mCurrentProperty = new VCardEntry.Property();
- }
-
- /**
- * Assume that VCard is not nested. In other words, this code does not accept
- */
- public void startEntry() {
- if (mCurrentContactStruct != null) {
- Log.e(LOG_TAG, "Nested VCard code is not supported now.");
- }
- mCurrentContactStruct = new VCardEntry(mVCardType, mAccount);
- }
-
- public void endEntry() {
- mCurrentContactStruct.consolidateFields();
- for (VCardEntryHandler entryHandler : mEntryHandlers) {
- entryHandler.onEntryCreated(mCurrentContactStruct);
- }
- mCurrentContactStruct = null;
- }
-
- public void startProperty() {
- mCurrentProperty.clear();
- }
-
- public void endProperty() {
- mCurrentContactStruct.addProperty(mCurrentProperty);
- }
-
- public void propertyName(String name) {
- mCurrentProperty.setPropertyName(name);
- }
-
- public void propertyGroup(String group) {
- }
-
- public void propertyParamType(String type) {
- if (mParamType != null) {
- Log.e(LOG_TAG, "propertyParamType() is called more than once " +
- "before propertyParamValue() is called");
- }
- mParamType = type;
- }
-
- public void propertyParamValue(String value) {
- if (mParamType == null) {
- // From vCard 2.1 specification. vCard 3.0 formally does not allow this case.
- mParamType = "TYPE";
- }
- mCurrentProperty.addParameter(mParamType, value);
- mParamType = null;
- }
-
- private String encodeString(String originalString, String charsetForDecodedBytes) {
- if (mInputCharset.equalsIgnoreCase(charsetForDecodedBytes)) {
- return originalString;
- }
- Charset charset = Charset.forName(mInputCharset);
- 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, charsetForDecodedBytes);
- } catch (UnsupportedEncodingException e) {
- Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes);
- return null;
- }
- }
-
- private String handleOneValue(String value, String charsetForDecodedBytes, String encoding) {
- if (encoding != null) {
- if (encoding.equals("BASE64") || encoding.equals("B")) {
- mCurrentProperty.setPropertyBytes(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(mInputCharset);
- } catch (UnsupportedEncodingException e1) {
- Log.e(LOG_TAG, "Failed to encode: charset=" + mInputCharset);
- 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, charsetForDecodedBytes);
- } catch (UnsupportedEncodingException e) {
- Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes);
- return new String(bytes);
- }
- }
- // Unknown encoding. Fall back to default.
- }
- return encodeString(value, charsetForDecodedBytes);
- }
-
- public void propertyValues(List<String> values) {
- if (values == null || values.isEmpty()) {
- return;
- }
-
- final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET");
- final String charset =
- ((charsetCollection != null) ? charsetCollection.iterator().next() : null);
- final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING");
- final String encoding =
- ((encodingCollection != null) ? encodingCollection.iterator().next() : null);
-
- String charsetForDecodedBytes = CharsetUtils.nameForDefaultVendor(charset);
- if (charsetForDecodedBytes == null || charsetForDecodedBytes.length() == 0) {
- charsetForDecodedBytes = mCharsetForDecodedBytes;
- }
-
- for (final String value : values) {
- mCurrentProperty.addToPropertyValueList(
- handleOneValue(value, charsetForDecodedBytes, encoding));
- }
- }
-
- public void showPerformanceInfo() {
- Log.d(LOG_TAG, "time for insert ContactStruct to database: " +
- mTimePushIntoContentResolver + " ms");
- }
-}
diff --git a/core/java/android/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java
deleted file mode 100644
index 7bab50d..0000000
--- a/core/java/android/pim/vcard/VCardEntryCounter.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import java.util.List;
-
-/**
- * The class which just counts the number of vCard entries in the specified input.
- */
-public class VCardEntryCounter implements VCardInterpreter {
- private int mCount;
-
- public int getCount() {
- return mCount;
- }
-
- public void start() {
- }
-
- public void end() {
- }
-
- public void startEntry() {
- }
-
- public void endEntry() {
- 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) {
- }
-}
diff --git a/core/java/android/pim/vcard/VCardEntryHandler.java b/core/java/android/pim/vcard/VCardEntryHandler.java
deleted file mode 100644
index 83a67fe..0000000
--- a/core/java/android/pim/vcard/VCardEntryHandler.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-/**
- * The interface called by {@link VCardEntryConstructor}. Useful when you don't want to
- * handle detailed information as what {@link VCardParser} provides via {@link VCardInterpreter}.
- */
-public interface VCardEntryHandler {
- /**
- * Called when the parsing started.
- */
- public void onStart();
-
- /**
- * The method called when one VCard entry is successfully created
- */
- public void onEntryCreated(final VCardEntry entry);
-
- /**
- * Called when the parsing ended.
- * Able to be use this method for showing performance log, etc.
- */
- public void onEnd();
-}
diff --git a/core/java/android/pim/vcard/VCardInterpreter.java b/core/java/android/pim/vcard/VCardInterpreter.java
deleted file mode 100644
index b5237c0..0000000
--- a/core/java/android/pim/vcard/VCardInterpreter.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import java.util.List;
-
-/**
- * <P>
- * The interface which should be implemented by the classes which have to analyze each
- * vCard entry more minutely than {@link VCardEntry} class analysis.
- * </P>
- * <P>
- * Here, there are several terms specific to vCard (and this library).
- * </P>
- * <P>
- * The term "entry" is one vCard representation in the input, which should start with "BEGIN:VCARD"
- * and end with "END:VCARD".
- * </P>
- * <P>
- * The term "property" is one line in vCard entry, which consists of "group", "property name",
- * "parameter(param) names and values", and "property values".
- * </P>
- * <P>
- * e.g. group1.propName;paramName1=paramValue1;paramName2=paramValue2;propertyValue1;propertyValue2...
- * </P>
- */
-public interface VCardInterpreter {
- /**
- * Called when vCard interpretation started.
- */
- void start();
-
- /**
- * Called when vCard interpretation finished.
- */
- void end();
-
- /**
- * Called when parsing one vCard entry started.
- * More specifically, this method is called when "BEGIN:VCARD" is read.
- */
- void startEntry();
-
- /**
- * Called when parsing one vCard entry ended.
- * More specifically, this method is called when "END:VCARD" is read.
- * Note that {@link #startEntry()} may be called since
- * vCard (especially 2.1) allows nested vCard.
- */
- void endEntry();
-
- /**
- * Called when reading one property started.
- */
- void startProperty();
-
- /**
- * Called when reading one property ended.
- */
- void endProperty();
-
- /**
- * @param group A group name. This method may be called more than once or may not be
- * called at all, depending on how many gruoups are appended to the property.
- */
- void propertyGroup(String group);
-
- /**
- * @param name A property name like "N", "FN", "ADR", etc.
- */
- void propertyName(String name);
-
- /**
- * @param type A parameter name like "ENCODING", "CHARSET", etc.
- */
- void propertyParamType(String type);
-
- /**
- * @param value A parameter value. This method may be called without
- * {@link #propertyParamType(String)} being called (when the vCard is vCard 2.1).
- */
- void propertyParamValue(String value);
-
- /**
- * @param values List of property values. The size of values would be 1 unless
- * coressponding property name is "N", "ADR", or "ORG".
- */
- void propertyValues(List<String> values);
-}
diff --git a/core/java/android/pim/vcard/VCardInterpreterCollection.java b/core/java/android/pim/vcard/VCardInterpreterCollection.java
deleted file mode 100644
index 99f81f7..0000000
--- a/core/java/android/pim/vcard/VCardInterpreterCollection.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import java.util.Collection;
-import java.util.List;
-
-/**
- * The {@link VCardInterpreter} implementation which aggregates more than one
- * {@link VCardInterpreter} objects and make a user object treat them as one
- * {@link VCardInterpreter} object.
- */
-public class VCardInterpreterCollection implements VCardInterpreter {
- private final Collection<VCardInterpreter> mInterpreterCollection;
-
- public VCardInterpreterCollection(Collection<VCardInterpreter> interpreterCollection) {
- mInterpreterCollection = interpreterCollection;
- }
-
- public Collection<VCardInterpreter> getCollection() {
- return mInterpreterCollection;
- }
-
- public void start() {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.start();
- }
- }
-
- public void end() {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.end();
- }
- }
-
- public void startEntry() {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.startEntry();
- }
- }
-
- public void endEntry() {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.endEntry();
- }
- }
-
- public void startProperty() {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.startProperty();
- }
- }
-
- public void endProperty() {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.endProperty();
- }
- }
-
- public void propertyGroup(String group) {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.propertyGroup(group);
- }
- }
-
- public void propertyName(String name) {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.propertyName(name);
- }
- }
-
- public void propertyParamType(String type) {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.propertyParamType(type);
- }
- }
-
- public void propertyParamValue(String value) {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.propertyParamValue(value);
- }
- }
-
- public void propertyValues(List<String> values) {
- for (VCardInterpreter builder : mInterpreterCollection) {
- builder.propertyValues(values);
- }
- }
-}
diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java
deleted file mode 100644
index 57c52a6..0000000
--- a/core/java/android/pim/vcard/VCardParser.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.pim.vcard.exception.VCardException;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public abstract class VCardParser {
- protected final int mParseType;
- protected boolean mCanceled;
-
- public VCardParser() {
- this(VCardConfig.PARSE_TYPE_UNKNOWN);
- }
-
- public VCardParser(int parseType) {
- mParseType = parseType;
- }
-
- /**
- * <P>
- * Parses the given stream and send the VCard data into VCardBuilderBase object.
- * </P.
- * <P>
- * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets
- * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is
- * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1,
- * In some exreme case, some VCard may have different charsets in one VCard (though
- * we do not see any device which emits such kind of malicious data)
- * </P>
- * <P>
- * In order to avoid "misunderstanding" charset as much as possible, this method
- * use "ISO-8859-1" for reading the stream. When charset is specified in some property
- * (with "CHARSET=..." parameter), the string is decoded to raw bytes and encoded to
- * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit
- * characters, which is not completely sure. In some cases, this "decoding-encoding"
- * scheme may fail. To avoid the case,
- * </P>
- * <P>
- * We recommend you to use {@link VCardSourceDetector} and detect which kind of source the
- * VCard comes from and explicitly specify a charset using the result.
- * </P>
- *
- * @param is The source to parse.
- * @param interepreter A {@link VCardInterpreter} object which used to construct data.
- * @return Returns true for success. Otherwise returns false.
- * @throws IOException, VCardException
- */
- public abstract boolean parse(InputStream is, VCardInterpreter interepreter)
- throws IOException, VCardException;
-
- /**
- * <P>
- * The method variants which accept charset.
- * </P>
- * <P>
- * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use
- * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese
- * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses
- * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification (e.g. W53K).
- * </P>
- *
- * @param is The source to parse.
- * @param charset Charset to be used.
- * @param builder The VCardBuilderBase object.
- * @return Returns true when successful. Otherwise returns false.
- * @throws IOException, VCardException
- */
- public abstract boolean parse(InputStream is, String charset, VCardInterpreter builder)
- throws IOException, VCardException;
-
- /**
- * The method variants which tells this object the operation is already canceled.
- */
- public abstract void parse(InputStream is, String charset,
- VCardInterpreter builder, boolean canceled)
- throws IOException, VCardException;
-
- /**
- * Cancel parsing.
- * Actual cancel is done after the end of the current one vcard entry parsing.
- */
- public void cancel() {
- mCanceled = true;
- }
-}
diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java
deleted file mode 100644
index fe8cfb0..0000000
--- a/core/java/android/pim/vcard/VCardParser_V21.java
+++ /dev/null
@@ -1,936 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.pim.vcard.exception.VCardAgentNotSupportedException;
-import android.pim.vcard.exception.VCardException;
-import android.pim.vcard.exception.VCardInvalidCommentLineException;
-import android.pim.vcard.exception.VCardInvalidLineException;
-import android.pim.vcard.exception.VCardNestedException;
-import android.pim.vcard.exception.VCardVersionException;
-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.Set;
-
-/**
- * This class is used to parse vCard. Please refer to vCard Specification 2.1 for more detail.
- */
-public class VCardParser_V21 extends VCardParser {
- private static final String LOG_TAG = "VCardParser_V21";
-
- /** Store the known-type */
- private static final HashSet<String> sKnownTypeSet = new HashSet<String>(
- Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK",
- "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS",
- "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK",
- "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL",
- "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF",
- "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 names available in vCard 2.1 */
- private static final HashSet<String> sAvailablePropertyNameSetV21 =
- new HashSet<String>(Arrays.asList(
- "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
- "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL",
- "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...
- */
- private static final HashSet<String> sAvailableEncodingV21 =
- new HashSet<String>(Arrays.asList(
- "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B"));
-
- // Used only for parsing END:VCARD.
- private String mPreviousLine;
-
- /** The builder to build parsed data */
- protected VCardInterpreter mBuilder = null;
-
- /**
- * The encoding type. "Encoding" in vCard is different from "Charset".
- * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE.
- */
- protected String mEncoding = null;
-
- protected final String sDefaultEncoding = "8BIT";
-
- // Should not directly read a line from this object. Use getLine() instead.
- protected BufferedReader mReader;
-
- // 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 Set<String> mUnknownTypeMap = new HashSet<String>();
- protected Set<String> mUnknownValueMap = new HashSet<String>();
-
- // For measuring performance.
- private long mTimeTotal;
- private long mTimeReadStartRecord;
- private long mTimeReadEndRecord;
- private long mTimeStartProperty;
- private long mTimeEndProperty;
- private long mTimeParseItems;
- private long mTimeParseLineAndHandleGroup;
- private long mTimeParsePropertyValues;
- private long mTimeParseAdrOrgN;
- private long mTimeHandleMiscPropertyValue;
- private long mTimeHandleQuotedPrintable;
- private long mTimeHandleBase64;
-
- public VCardParser_V21() {
- this(null);
- }
-
- public VCardParser_V21(VCardSourceDetector detector) {
- this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN);
- }
-
- public VCardParser_V21(int parseType) {
- super(parseType);
- if (parseType == VCardConfig.PARSE_TYPE_FOMA) {
- mNestCount = 1;
- }
- }
-
- /**
- * Parses the file at the given position.
- *
- * vcard_file = [wsls] vcard [wsls]
- */
- protected void parseVCardFile() throws IOException, VCardException {
- 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;
- }
- }
- }
-
- protected int getVersion() {
- return VCardConfig.FLAG_V21;
- }
-
- protected String getVersionString() {
- return VCardConstants.VERSION_V21;
- }
-
- /**
- * @return true when the propertyName is a valid property name.
- */
- protected boolean isValidPropertyName(String propertyName) {
- if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) ||
- propertyName.startsWith("X-")) &&
- !mUnknownTypeMap.contains(propertyName)) {
- mUnknownTypeMap.add(propertyName);
- Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName);
- }
- return true;
- }
-
- /**
- * @return true when the encoding is a valid encoding.
- */
- protected boolean isValidEncoding(String encoding) {
- return sAvailableEncodingV21.contains(encoding.toUpperCase());
- }
-
- /**
- * @return String. It may be null, or its length may be 0
- * @throws IOException
- */
- protected String getLine() throws IOException {
- return mReader.readLine();
- }
-
- /**
- * @return String with it's length > 0
- * @throws IOException
- * @throws VCardException when the stream reached end of line
- */
- protected String getNonEmptyLine() throws IOException, VCardException {
- String line;
- while (true) {
- line = getLine();
- if (line == null) {
- throw new VCardException("Reached end of buffer.");
- } else if (line.trim().length() > 0) {
- return line;
- }
- }
- }
-
- /**
- * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF
- * items *CRLF
- * "END" [ws] ":" [ws] "VCARD"
- */
- 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.startEntry();
- mTimeReadStartRecord += System.currentTimeMillis() - start;
- }
- start = System.currentTimeMillis();
- parseItems();
- mTimeParseItems += System.currentTimeMillis() - start;
- readEndVCard(true, false);
- if (mBuilder != null) {
- start = System.currentTimeMillis();
- mBuilder.endEntry();
- mTimeReadEndRecord += System.currentTimeMillis() - start;
- }
- return true;
- }
-
- /**
- * @return True when successful. False when reaching the end of line
- * @throws IOException
- * @throws VCardException
- */
- protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
- String line;
- do {
- while (true) {
- line = getLine();
- if (line == null) {
- return false;
- } else if (line.trim().length() > 0) {
- break;
- }
- }
- String[] strArray = line.split(":", 2);
- int length = strArray.length;
-
- // Though vCard 2.1/3.0 specification does not allow lower cases,
- // vCard file emitted by some external vCard expoter have such invalid Strings.
- // So we allow it.
- // e.g. BEGIN:vCard
- 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.");
- }
-
- /**
- * 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);
- }
-
- /**
- * items = *CRLF item
- * / item
- */
- protected void parseItems() throws IOException, VCardException {
- boolean ended = false;
-
- if (mBuilder != null) {
- long start = System.currentTimeMillis();
- mBuilder.startProperty();
- mTimeStartProperty += System.currentTimeMillis() - start;
- }
- 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();
- } catch (VCardInvalidCommentLineException e) {
- Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored.");
- ended = false;
- }
- 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
- * / [groups "."] "ORG" [params] ":" orgparts CRLF
- * / [groups "."] "N" [params] ":" nameparts CRLF
- * / [groups "."] "AGENT" [params] ":" vcard CRLF
- */
- protected boolean parseItem() throws IOException, VCardException {
- mEncoding = sDefaultEncoding;
-
- final String line = getNonEmptyLine();
- long start = System.currentTimeMillis();
-
- String[] propertyNameAndValue = separateLineAndHandleGroup(line);
- if (propertyNameAndValue == null) {
- return true;
- }
- if (propertyNameAndValue.length != 2) {
- throw new VCardInvalidLineException("Invalid line \"" + line + "\"");
- }
- String propertyName = propertyNameAndValue[0].toUpperCase();
- String propertyValue = propertyNameAndValue[1];
-
- mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start;
-
- if (propertyName.equals("ADR") || propertyName.equals("ORG") ||
- propertyName.equals("N")) {
- start = System.currentTimeMillis();
- handleMultiplePropertyValue(propertyName, propertyValue);
- mTimeParseAdrOrgN += 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(getVersionString())) {
- throw new VCardVersionException("Incompatible version: " +
- propertyValue + " != " + getVersionString());
- }
- start = System.currentTimeMillis();
- handlePropertyValue(propertyName, propertyValue);
- mTimeParsePropertyValues += 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.0 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 state = STATE_GROUP_OR_PROPNAME;
- int nameIndex = 0;
-
- final String[] propertyNameAndValue = new String[2];
-
- final int length = line.length();
- if (length > 0 && line.charAt(0) == '#') {
- throw new VCardInvalidCommentLineException();
- }
-
- for (int i = 0; i < length; i++) {
- char ch = line.charAt(i);
- switch (state) {
- case STATE_GROUP_OR_PROPNAME: {
- if (ch == ':') {
- final 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 VCardInvalidLineException("Invalid line: \"" + line + "\"");
- }
-
- /**
- * params = ";" [ws] paramlist
- * paramlist = paramlist [ws] ";" [ws] param
- * / param
- * param = "TYPE" [ws] "=" [ws] ptypeval
- * / "VALUE" [ws] "=" [ws] pvalueval
- * / "ENCODING" [ws] "=" [ws] pencodingval
- * / "CHARSET" [ws] "=" [ws] charsetval
- * / "LANGUAGE" [ws] "=" [ws] langval
- * / "X-" word [ws] "=" [ws] word
- * / knowntype
- */
- protected void handleParams(String params) throws VCardException {
- String[] strArray = params.split("=", 2);
- if (strArray.length == 2) {
- final String paramName = strArray[0].trim().toUpperCase();
- String paramValue = strArray[1].trim();
- if (paramName.equals("TYPE")) {
- handleType(paramValue);
- } else if (paramName.equals("VALUE")) {
- handleValue(paramValue);
- } else if (paramName.equals("ENCODING")) {
- handleEncoding(paramValue);
- } else if (paramName.equals("CHARSET")) {
- handleCharset(paramValue);
- } else if (paramName.equals("LANGUAGE")) {
- handleLanguage(paramValue);
- } else if (paramName.startsWith("X-")) {
- handleAnyParam(paramName, paramValue);
- } else {
- throw new VCardException("Unknown type \"" + paramName + "\"");
- }
- } else {
- handleParamWithoutName(strArray[0]);
- }
- }
-
- /**
- * vCard 3.0 parser may throw VCardException.
- */
- @SuppressWarnings("unused")
- protected void handleParamWithoutName(final String paramValue) throws VCardException {
- handleType(paramValue);
- }
-
- /**
- * ptypeval = knowntype / "X-" word
- */
- protected void handleType(final String ptypeval) {
- String upperTypeValue = ptypeval;
- if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) &&
- !mUnknownTypeMap.contains(ptypeval)) {
- mUnknownTypeMap.add(ptypeval);
- Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval);
- }
- if (mBuilder != null) {
- mBuilder.propertyParamType("TYPE");
- mBuilder.propertyParamValue(upperTypeValue);
- }
- }
-
- /**
- * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word
- */
- protected void handleValue(final String pvalueval) {
- if (!sKnownValueSet.contains(pvalueval.toUpperCase()) &&
- pvalueval.startsWith("X-") &&
- !mUnknownValueMap.contains(pvalueval)) {
- mUnknownValueMap.add(pvalueval);
- Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval);
- }
- if (mBuilder != null) {
- mBuilder.propertyParamType("VALUE");
- mBuilder.propertyParamValue(pvalueval);
- }
- }
-
- /**
- * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word
- */
- protected void handleEncoding(String pencodingval) throws VCardException {
- if (isValidEncoding(pencodingval) ||
- pencodingval.startsWith("X-")) {
- if (mBuilder != null) {
- mBuilder.propertyParamType("ENCODING");
- mBuilder.propertyParamValue(pencodingval);
- }
- mEncoding = pencodingval;
- } else {
- throw new VCardException("Unknown encoding \"" + pencodingval + "\"");
- }
- }
-
- /**
- * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521),
- * but today's vCard often contains other charset, so we allow them.
- */
- protected void handleCharset(String charsetval) {
- if (mBuilder != null) {
- mBuilder.propertyParamType("CHARSET");
- mBuilder.propertyParamValue(charsetval);
- }
- }
-
- /**
- * See also Section 7.1 of RFC 1521
- */
- protected void handleLanguage(String langval) throws VCardException {
- String[] strArray = langval.split("-");
- if (strArray.length != 2) {
- throw new VCardException("Invalid Language: \"" + langval + "\"");
- }
- String tmp = strArray[0];
- int length = tmp.length();
- for (int i = 0; i < length; i++) {
- if (!isLetter(tmp.charAt(i))) {
- throw new VCardException("Invalid Language: \"" + langval + "\"");
- }
- }
- tmp = strArray[1];
- length = tmp.length();
- for (int i = 0; i < length; i++) {
- if (!isLetter(tmp.charAt(i))) {
- throw new VCardException("Invalid Language: \"" + langval + "\"");
- }
- }
- if (mBuilder != null) {
- mBuilder.propertyParamType("LANGUAGE");
- mBuilder.propertyParamValue(langval);
- }
- }
-
- /**
- * Mainly for "X-" type. This accepts any kind of type without check.
- */
- protected void handleAnyParam(String paramName, String paramValue) {
- if (mBuilder != null) {
- mBuilder.propertyParamType(paramName);
- mBuilder.propertyParamValue(paramValue);
- }
- }
-
- protected void handlePropertyValue(String propertyName, String propertyValue)
- throws IOException, VCardException {
- if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
- final long start = System.currentTimeMillis();
- final String result = getQuotedPrintable(propertyValue);
- if (mBuilder != null) {
- ArrayList<String> v = new ArrayList<String>();
- v.add(result);
- mBuilder.propertyValues(v);
- }
- mTimeHandleQuotedPrintable += System.currentTimeMillis() - start;
- } else if (mEncoding.equalsIgnoreCase("BASE64") ||
- mEncoding.equalsIgnoreCase("B")) {
- final 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 {
- final 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);
- }
- }
- mTimeHandleBase64 += 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 + "\".");
- }
-
- final long start = System.currentTimeMillis();
- if (mBuilder != null) {
- ArrayList<String> v = new ArrayList<String>();
- v.add(maybeUnescapeText(propertyValue));
- mBuilder.propertyValues(v);
- }
- mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start;
- }
- }
-
- protected String getQuotedPrintable(String firstString) throws IOException, VCardException {
- // Specifically, there may be some padding between = and CRLF.
- // See the following:
- //
- // qp-line := *(qp-segment transport-padding CRLF)
- // qp-part transport-padding
- // qp-segment := qp-section *(SPACE / TAB) "="
- // ; Maximum length of 76 characters
- //
- // e.g. (from RFC 2045)
- // Now's the time =
- // for all folk to come=
- // to the aid of their country.
- if (firstString.trim().endsWith("=")) {
- // remove "transport-padding"
- int pos = firstString.length() - 1;
- while(firstString.charAt(pos) != '=') {
- }
- StringBuilder builder = new StringBuilder();
- builder.append(firstString.substring(0, pos + 1));
- builder.append("\r\n");
- String line;
- while (true) {
- line = getLine();
- if (line == null) {
- throw new VCardException(
- "File ended during parsing quoted-printable String");
- }
- if (line.trim().endsWith("=")) {
- // remove "transport-padding"
- pos = line.length() - 1;
- while(line.charAt(pos) != '=') {
- }
- builder.append(line.substring(0, pos + 1));
- builder.append("\r\n");
- } else {
- builder.append(line);
- break;
- }
- }
- return builder.toString();
- } else {
- return firstString;
- }
- }
-
- protected String getBase64(String firstString) throws IOException, VCardException {
- StringBuilder builder = new StringBuilder();
- builder.append(firstString);
-
- while (true) {
- String line = getLine();
- if (line == null) {
- throw new VCardException(
- "File ended during parsing BASE64 binary");
- }
- if (line.length() == 0) {
- break;
- }
- builder.append(line);
- }
-
- return builder.toString();
- }
-
- /**
- * Mainly for "ADR", "ORG", and "N"
- * We do not care the number of strnosemi here.
- *
- * addressparts = 0*6(strnosemi ";") strnosemi
- * ; PO Box, Extended Addr, Street, Locality, Region,
- * Postal Code, Country Name
- * orgparts = *(strnosemi ";") strnosemi
- * ; First is Organization Name,
- * remainder are Organization Units.
- * nameparts = 0*4(strnosemi ";") strnosemi
- * ; Family, Given, Middle, Prefix, Suffix.
- * ; Example:Public;John;Q.;Reverend Dr.;III, Esq.
- * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi
- * ; To include a semicolon in this string, it must be escaped
- * ; with a "\" character.
- *
- * We are not sure whether we should add "\" CRLF to each value.
- * For now, we exclude them.
- */
- protected void handleMultiplePropertyValue(String propertyName, String propertyValue)
- throws IOException, VCardException {
- // vCard 2.1 does not allow QUOTED-PRINTABLE here,
- // but some softwares/devices emit such data.
- if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
- propertyValue = getQuotedPrintable(propertyValue);
- }
-
- if (mBuilder != null) {
- mBuilder.propertyValues(VCardUtils.constructListFromValue(
- propertyValue, (getVersion() == VCardConfig.FLAG_V30)));
- }
- }
-
- /**
- * 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(final String propertyValue) throws VCardException {
- if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) {
- // Apparently invalid line seen in Windows Mobile 6.5. Ignore them.
- return;
- } else {
- throw new VCardAgentNotSupportedException("AGENT Property is not supported now.");
- }
- // TODO: Support AGENT property.
- }
-
- /**
- * For vCard 3.0.
- */
- protected String maybeUnescapeText(final String text) {
- return 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 maybeUnescapeCharacter(final char ch) {
- return unescapeCharacter(ch);
- }
-
- public static String unescapeCharacter(final 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.
- if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') {
- return String.valueOf(ch);
- } else {
- return null;
- }
- }
-
- @Override
- public boolean parse(final InputStream is, final VCardInterpreter builder)
- throws IOException, VCardException {
- return parse(is, VCardConfig.DEFAULT_CHARSET, builder);
- }
-
- @Override
- public boolean parse(InputStream is, String charset, VCardInterpreter builder)
- throws IOException, VCardException {
- if (charset == null) {
- charset = VCardConfig.DEFAULT_CHARSET;
- }
- final InputStreamReader tmpReader = new InputStreamReader(is, charset);
- if (VCardConfig.showPerformanceLog()) {
- mReader = new CustomBufferedReader(tmpReader);
- } else {
- mReader = new BufferedReader(tmpReader);
- }
-
- mBuilder = builder;
-
- long start = System.currentTimeMillis();
- if (mBuilder != null) {
- mBuilder.start();
- }
- parseVCardFile();
- if (mBuilder != null) {
- mBuilder.end();
- }
- mTimeTotal += System.currentTimeMillis() - start;
-
- if (VCardConfig.showPerformanceLog()) {
- showPerformanceInfo();
- }
-
- return true;
- }
-
- @Override
- public void parse(InputStream is, String charset, VCardInterpreter builder, boolean canceled)
- throws IOException, VCardException {
- mCanceled = canceled;
- parse(is, charset, builder);
- }
-
- private void showPerformanceInfo() {
- 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, "Time for handling the beggining of the record: " +
- mTimeReadStartRecord + " ms");
- Log.d(LOG_TAG, "Time for handling the end of the record: " +
- mTimeReadEndRecord + " ms");
- Log.d(LOG_TAG, "Time for parsing line, and handling group: " +
- mTimeParseLineAndHandleGroup + " ms");
- Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms");
- Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms");
- Log.d(LOG_TAG, "Time for handling normal property values: " +
- mTimeHandleMiscPropertyValue + " ms");
- Log.d(LOG_TAG, "Time for handling Quoted-Printable: " +
- mTimeHandleQuotedPrintable + " ms");
- Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms");
- }
-
- private boolean isLetter(char ch) {
- if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
- return true;
- }
- 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/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java
deleted file mode 100644
index 4ecfe97..0000000
--- a/core/java/android/pim/vcard/VCardParser_V30.java
+++ /dev/null
@@ -1,358 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.pim.vcard.exception.VCardException;
-import android.util.Log;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
-
-/**
- * The class used to parse vCard 3.0.
- * 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> sAcceptablePropsWithParam = new HashSet<String>(
- Arrays.asList(
- "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",
- "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0
-
- // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety.
- private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>(
- Arrays.asList("7BIT", "8BIT", "BASE64", "B"));
-
- // Although RFC 2426 specifies some property must not have parameters, we allow it,
- // since there may be some careers which violates the RFC...
- private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>();
-
- private String mPreviousLine;
-
- private boolean mEmittedAgentWarning = false;
-
- /**
- * True when the caller wants the parser to be strict about the input.
- * Currently this is only for testing.
- */
- private final boolean mStrictParsing;
-
- public VCardParser_V30() {
- super();
- mStrictParsing = false;
- }
-
- /**
- * @param strictParsing when true, this object throws VCardException when the vcard is not
- * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class
- * is not fully yet for being used with this flag and may not notice invalid line(s).
- *
- * @hide currently only for testing!
- */
- public VCardParser_V30(boolean strictParsing) {
- super();
- mStrictParsing = strictParsing;
- }
-
- public VCardParser_V30(int parseMode) {
- super(parseMode);
- mStrictParsing = false;
- }
-
- @Override
- protected int getVersion() {
- return VCardConfig.FLAG_V30;
- }
-
- @Override
- protected String getVersionString() {
- return VCardConstants.VERSION_V30;
- }
-
- @Override
- protected boolean isValidPropertyName(String propertyName) {
- if (!(sAcceptablePropsWithParam.contains(propertyName) ||
- acceptablePropsWithoutParam.contains(propertyName) ||
- propertyName.startsWith("X-")) &&
- !mUnknownTypeMap.contains(propertyName)) {
- mUnknownTypeMap.add(propertyName);
- Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName);
- }
- return true;
- }
-
- @Override
- protected boolean isValidEncoding(String encoding) {
- return sAcceptableEncodingV30.contains(encoding.toUpperCase());
- }
-
- @Override
- protected String getLine() throws IOException {
- if (mPreviousLine != null) {
- String ret = mPreviousLine;
- mPreviousLine = null;
- return ret;
- } else {
- return mReader.readLine();
- }
- }
-
- /**
- * vCard 3.0 requires that the line with space at the beginning of the line
- * must be combined with previous line.
- */
- @Override
- protected String getNonEmptyLine() throws IOException, VCardException {
- String line;
- StringBuilder builder = null;
- while (true) {
- line = mReader.readLine();
- if (line == null) {
- if (builder != null) {
- return builder.toString();
- } else if (mPreviousLine != null) {
- String ret = mPreviousLine;
- mPreviousLine = null;
- return ret;
- }
- throw new VCardException("Reached end of buffer.");
- } else if (line.length() == 0) {
- if (builder != null) {
- return builder.toString();
- } else if (mPreviousLine != null) {
- String ret = mPreviousLine;
- mPreviousLine = null;
- return ret;
- }
- } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
- if (builder != null) {
- // 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();
- builder.append(mPreviousLine);
- mPreviousLine = null;
- builder.append(line.substring(1));
- } else {
- throw new VCardException("Space exists at the beginning of the line");
- }
- } else {
- if (mPreviousLine == null) {
- mPreviousLine = line;
- if (builder != null) {
- return builder.toString();
- }
- } else {
- String ret = mPreviousLine;
- mPreviousLine = line;
- return ret;
- }
- }
- }
- }
-
-
- /**
- * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF
- * 1 * (contentline)
- * ;A vCard object MUST include the VERSION, FN and N types.
- * [group "."] "END" ":" "VCARD" 1 * CRLF
- */
- @Override
- protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
- // TODO: vCard 3.0 supports group.
- return super.readBeginVCard(allowGarbage);
- }
-
- @Override
- protected void readEndVCard(boolean useCache, boolean allowGarbage)
- throws IOException, VCardException {
- // TODO: vCard 3.0 supports group.
- super.readEndVCard(useCache, allowGarbage);
- }
-
- /**
- * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
- */
- @Override
- protected void handleParams(String params) throws VCardException {
- try {
- super.handleParams(params);
- } catch (VCardException e) {
- // maybe IANA type
- String[] strArray = params.split("=", 2);
- if (strArray.length == 2) {
- handleAnyParam(strArray[0], strArray[1]);
- } else {
- // Must not come here in the current implementation.
- throw new VCardException(
- "Unknown params value: " + params);
- }
- }
- }
-
- @Override
- protected void handleAnyParam(String paramName, String paramValue) {
- super.handleAnyParam(paramName, paramValue);
- }
-
- @Override
- protected void handleParamWithoutName(final String paramValue) throws VCardException {
- if (mStrictParsing) {
- throw new VCardException("Parameter without name is not acceptable in vCard 3.0");
- } else {
- super.handleParamWithoutName(paramValue);
- }
- }
-
- /**
- * vCard 3.0 defines
- *
- * param = param-name "=" param-value *("," param-value)
- * param-name = iana-token / x-name
- * param-value = ptext / quoted-string
- * quoted-string = DQUOTE QSAFE-CHAR DQUOTE
- */
- @Override
- protected void handleType(String ptypevalues) {
- String[] ptypeArray = ptypevalues.split(",");
- mBuilder.propertyParamType("TYPE");
- for (String value : ptypeArray) {
- int length = value.length();
- if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
- mBuilder.propertyParamValue(value.substring(1, value.length() - 1));
- } else {
- mBuilder.propertyParamValue(value);
- }
- }
- }
-
- @Override
- protected void handleAgent(String propertyValue) {
- // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
- //
- // e.g.
- // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
- // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
- // ET:jfriday@host.com\nEND:VCARD\n
- //
- // TODO: fix this.
- //
- // issue:
- // vCard 3.0 also allows this as an example.
- //
- // AGENT;VALUE=uri:
- // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com
- //
- // This is not vCard. Should we support this?
- //
- // Just ignore the line for now, since we cannot know how to handle it...
- if (!mEmittedAgentWarning) {
- Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
- mEmittedAgentWarning = true;
- }
- }
-
- /**
- * vCard 3.0 does not require two CRLF at the last of BASE64 data.
- * It only requires that data should be MIME-encoded.
- */
- @Override
- protected String getBase64(String firstString) throws IOException, VCardException {
- StringBuilder builder = new StringBuilder();
- builder.append(firstString);
-
- while (true) {
- String line = getLine();
- if (line == null) {
- throw new VCardException(
- "File ended during parsing BASE64 binary");
- }
- if (line.length() == 0) {
- break;
- } else if (!line.startsWith(" ") && !line.startsWith("\t")) {
- mPreviousLine = line;
- break;
- }
- builder.append(line);
- }
-
- return builder.toString();
- }
-
- /**
- * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
- * ; \\ encodes \, \n or \N encodes newline
- * ; \; encodes ;, \, encodes ,
- *
- * Note: Apple escapes ':' into '\:' while does not escape '\'
- */
- @Override
- protected String maybeUnescapeText(String text) {
- return unescapeText(text);
- }
-
- public static String unescapeText(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("\n");
- } else {
- builder.append(next_ch);
- }
- } else {
- builder.append(ch);
- }
- }
- return builder.toString();
- }
-
- @Override
- protected String maybeUnescapeCharacter(char ch) {
- return unescapeCharacter(ch);
- }
-
- public static String unescapeCharacter(char ch) {
- if (ch == 'n' || ch == 'N') {
- return "\n";
- } else {
- return String.valueOf(ch);
- }
- }
-}
diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java
deleted file mode 100644
index 7297c50..0000000
--- a/core/java/android/pim/vcard/VCardSourceDetector.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-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 VCardInterpreter {
- 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 = VCardConfig.PARSE_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 startEntry() {
- }
-
- public void startProperty() {
- mNeedParseSpecifiedCharset = false;
- }
-
- public void endProperty() {
- }
-
- public void endEntry() {
- }
-
- public void propertyGroup(String group) {
- }
-
- public void propertyName(String name) {
- if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) {
- mType = VCardConfig.PARSE_TYPE_FOMA;
- mNeedParseSpecifiedCharset = true;
- return;
- }
- if (mType != VCardConfig.PARSE_TYPE_UNKNOWN) {
- return;
- }
- if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) {
- mType = VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP;
- } else if (FOMA_SIGNS.contains(name)) {
- mType = VCardConfig.PARSE_TYPE_FOMA;
- } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) {
- mType = VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP;
- } else if (APPLE_SIGNS.contains(name)) {
- mType = VCardConfig.PARSE_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);
- }
- }
-
- /* package */ int getEstimatedType() {
- 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 VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP:
- case VCardConfig.PARSE_TYPE_FOMA:
- case VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP:
- return "SHIFT_JIS";
- case VCardConfig.PARSE_TYPE_APPLE:
- return "UTF-8";
- default:
- return null;
- }
- }
-}
diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java
deleted file mode 100644
index 11b112b..0000000
--- a/core/java/android/pim/vcard/VCardUtils.java
+++ /dev/null
@@ -1,545 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard;
-
-import android.content.ContentProviderOperation;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Utilities for VCard handling codes.
- */
-public class VCardUtils {
- // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is
- // converted to two parameter Strings. These only contain some minor fields valid in both
- // vCard and current (as of 2009-08-07) Contacts structure.
- private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS;
- private static final Set<String> sPhoneTypesUnknownToContactsSet;
- private static final Map<String, Integer> sKnownPhoneTypeMap_StoI;
- private static final Map<Integer, String> sKnownImPropNameMap_ItoS;
- private static final Set<String> sMobilePhoneLabelSet;
-
- static {
- sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>();
- sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>();
-
- sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR);
- sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER);
- sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN);
-
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE);
-
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK,
- Phone.TYPE_CALLBACK);
- sKnownPhoneTypeMap_StoI.put(
- VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD,
- Phone.TYPE_TTY_TDD);
- sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT,
- Phone.TYPE_ASSISTANT);
-
- sPhoneTypesUnknownToContactsSet = new HashSet<String>();
- sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM);
- sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG);
- sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS);
- sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO);
-
- sKnownImPropNameMap_ItoS = new HashMap<Integer, String>();
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK,
- VCardConstants.PROPERTY_X_GOOGLE_TALK);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ);
- sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING);
-
- // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone)
- // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone)
- // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone)
- // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone)
- sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList(
- "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4",
- "\uFF79\uFF72\uFF80\uFF72"));
- }
-
- public static String getPhoneTypeString(Integer type) {
- return sKnownPhoneTypesMap_ItoS.get(type);
- }
-
- /**
- * Returns Interger when the given types can be parsed as known type. Returns String object
- * when not, which should be set to label.
- */
- public static Object getPhoneTypeFromStrings(Collection<String> types,
- String number) {
- if (number == null) {
- number = "";
- }
- int type = -1;
- String label = null;
- boolean isFax = false;
- boolean hasPref = false;
-
- if (types != null) {
- for (String typeString : types) {
- if (typeString == null) {
- continue;
- }
- typeString = typeString.toUpperCase();
- if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
- hasPref = true;
- } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) {
- isFax = true;
- } else {
- if (typeString.startsWith("X-") && type < 0) {
- typeString = typeString.substring(2);
- }
- if (typeString.length() == 0) {
- continue;
- }
- final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString);
- if (tmp != null) {
- final int typeCandidate = tmp;
- // TYPE_PAGER is prefered when the number contains @ surronded by
- // a pager number and a domain name.
- // e.g.
- // o 1111@domain.com
- // x @domain.com
- // x 1111@
- final int indexOfAt = number.indexOf("@");
- if ((typeCandidate == Phone.TYPE_PAGER
- && 0 < indexOfAt && indexOfAt < number.length() - 1)
- || type < 0
- || type == Phone.TYPE_CUSTOM) {
- type = tmp;
- }
- } else if (type < 0) {
- type = Phone.TYPE_CUSTOM;
- label = typeString;
- }
- }
- }
- }
- if (type < 0) {
- if (hasPref) {
- type = Phone.TYPE_MAIN;
- } else {
- // default to TYPE_HOME
- type = Phone.TYPE_HOME;
- }
- }
- if (isFax) {
- if (type == Phone.TYPE_HOME) {
- type = Phone.TYPE_FAX_HOME;
- } else if (type == Phone.TYPE_WORK) {
- type = Phone.TYPE_FAX_WORK;
- } else if (type == Phone.TYPE_OTHER) {
- type = Phone.TYPE_OTHER_FAX;
- }
- }
- if (type == Phone.TYPE_CUSTOM) {
- return label;
- } else {
- return type;
- }
- }
-
- @SuppressWarnings("deprecation")
- public static boolean isMobilePhoneLabel(final String label) {
- // For backward compatibility.
- // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now.
- // To support mobile type at that time, this custom label had been used.
- return (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME.equals(label)
- || sMobilePhoneLabelSet.contains(label));
- }
-
- public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) {
- return sPhoneTypesUnknownToContactsSet.contains(label);
- }
-
- public static String getPropertyNameForIm(final int protocol) {
- return sKnownImPropNameMap_ItoS.get(protocol);
- }
-
- public static String[] sortNameElements(final int vcardType,
- final String familyName, final String middleName, final String givenName) {
- final String[] list = new String[3];
- final int nameOrderType = VCardConfig.getNameOrderType(vcardType);
- switch (nameOrderType) {
- case VCardConfig.NAME_ORDER_JAPANESE: {
- if (containsOnlyPrintableAscii(familyName) &&
- containsOnlyPrintableAscii(givenName)) {
- list[0] = givenName;
- list[1] = middleName;
- list[2] = familyName;
- } else {
- list[0] = familyName;
- list[1] = middleName;
- list[2] = givenName;
- }
- break;
- }
- case VCardConfig.NAME_ORDER_EUROPE: {
- list[0] = middleName;
- list[1] = givenName;
- list[2] = familyName;
- break;
- }
- default: {
- list[0] = givenName;
- list[1] = middleName;
- list[2] = familyName;
- break;
- }
- }
- return list;
- }
-
- public static int getPhoneNumberFormat(final int vcardType) {
- if (VCardConfig.isJapaneseDevice(vcardType)) {
- return PhoneNumberUtils.FORMAT_JAPAN;
- } else {
- return PhoneNumberUtils.FORMAT_NANP;
- }
- }
-
- /**
- * Inserts postal data into the builder object.
- *
- * Note that the data structure of ContactsContract is different from that defined in vCard.
- * So some conversion may be performed in this method.
- */
- public static void insertStructuredPostalDataUsingContactsStruct(int vcardType,
- final ContentProviderOperation.Builder builder,
- final VCardEntry.PostalData postalData) {
- builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0);
- builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
-
- builder.withValue(StructuredPostal.TYPE, postalData.type);
- if (postalData.type == StructuredPostal.TYPE_CUSTOM) {
- builder.withValue(StructuredPostal.LABEL, postalData.label);
- }
-
- final String streetString;
- if (TextUtils.isEmpty(postalData.street)) {
- if (TextUtils.isEmpty(postalData.extendedAddress)) {
- streetString = null;
- } else {
- streetString = postalData.extendedAddress;
- }
- } else {
- if (TextUtils.isEmpty(postalData.extendedAddress)) {
- streetString = postalData.street;
- } else {
- streetString = postalData.street + " " + postalData.extendedAddress;
- }
- }
- builder.withValue(StructuredPostal.POBOX, postalData.pobox);
- builder.withValue(StructuredPostal.STREET, streetString);
- builder.withValue(StructuredPostal.CITY, postalData.localty);
- builder.withValue(StructuredPostal.REGION, postalData.region);
- builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode);
- builder.withValue(StructuredPostal.COUNTRY, postalData.country);
-
- builder.withValue(StructuredPostal.FORMATTED_ADDRESS,
- postalData.getFormattedAddress(vcardType));
- if (postalData.isPrimary) {
- builder.withValue(Data.IS_PRIMARY, 1);
- }
- }
-
- public static String constructNameFromElements(final int vcardType,
- final String familyName, final String middleName, final String givenName) {
- return constructNameFromElements(vcardType, familyName, middleName, givenName,
- null, null);
- }
-
- public static String constructNameFromElements(final int vcardType,
- final String familyName, final String middleName, final String givenName,
- final String prefix, final String suffix) {
- final StringBuilder builder = new StringBuilder();
- final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName);
- boolean first = true;
- if (!TextUtils.isEmpty(prefix)) {
- first = false;
- builder.append(prefix);
- }
- for (final String namePart : nameList) {
- if (!TextUtils.isEmpty(namePart)) {
- if (first) {
- first = false;
- } else {
- builder.append(' ');
- }
- builder.append(namePart);
- }
- }
- if (!TextUtils.isEmpty(suffix)) {
- if (!first) {
- builder.append(' ');
- }
- builder.append(suffix);
- }
- return builder.toString();
- }
-
- public static List<String> constructListFromValue(final String value,
- final boolean isV30) {
- final List<String> list = new ArrayList<String>();
- 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);
- final String unescapedString =
- (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) :
- VCardParser_V21.unescapeCharacter(nextCh));
- if (unescapedString != null) {
- builder.append(unescapedString);
- i++;
- } else {
- builder.append(ch);
- }
- } else if (ch == ';') {
- list.add(builder.toString());
- builder = new StringBuilder();
- } else {
- builder.append(ch);
- }
- }
- list.add(builder.toString());
- return list;
- }
-
- public static boolean containsOnlyPrintableAscii(final String...values) {
- if (values == null) {
- return true;
- }
- return containsOnlyPrintableAscii(Arrays.asList(values));
- }
-
- public static boolean containsOnlyPrintableAscii(final Collection<String> values) {
- if (values == null) {
- return true;
- }
- for (final String value : values) {
- if (TextUtils.isEmpty(value)) {
- continue;
- }
- if (!TextUtils.isPrintableAsciiOnly(value)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * This is useful when checking the string should be encoded into quoted-printable
- * or not, which is required by vCard 2.1.
- * See the definition of "7bit" in vCard 2.1 spec for more information.
- */
- public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) {
- if (values == null) {
- return true;
- }
- return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values));
- }
-
- public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) {
- if (values == null) {
- return true;
- }
- final int asciiFirst = 0x20;
- final int asciiLast = 0x7E; // included
- for (final String value : values) {
- if (TextUtils.isEmpty(value)) {
- continue;
- }
- final int length = value.length();
- for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
- final int c = value.codePointAt(i);
- if (!(asciiFirst <= c && c <= asciiLast)) {
- return false;
- }
- }
- }
- return true;
- }
-
- private static final Set<Character> sUnAcceptableAsciiInV21WordSet =
- new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' '));
-
- /**
- * This is useful since vCard 3.0 often requires the ("X-") properties and groups
- * should contain only alphabets, digits, and hyphen.
- *
- * Note: It is already known some devices (wrongly) outputs properties with characters
- * which should not be in the field. One example is "X-GOOGLE TALK". We accept
- * such kind of input but must never output it unless the target is very specific
- * to the device which is able to parse the malformed input.
- */
- public static boolean containsOnlyAlphaDigitHyphen(final String...values) {
- if (values == null) {
- return true;
- }
- return containsOnlyAlphaDigitHyphen(Arrays.asList(values));
- }
-
- public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) {
- if (values == null) {
- return true;
- }
- final int upperAlphabetFirst = 0x41; // A
- final int upperAlphabetAfterLast = 0x5b; // [
- final int lowerAlphabetFirst = 0x61; // a
- final int lowerAlphabetAfterLast = 0x7b; // {
- final int digitFirst = 0x30; // 0
- final int digitAfterLast = 0x3A; // :
- final int hyphen = '-';
- for (final String str : values) {
- if (TextUtils.isEmpty(str)) {
- continue;
- }
- final int length = str.length();
- for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
- int codepoint = str.codePointAt(i);
- if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) ||
- (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) ||
- (digitFirst <= codepoint && codepoint < digitAfterLast) ||
- (codepoint == hyphen))) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * <P>
- * Returns true when the given String is categorized as "word" specified in vCard spec 2.1.
- * </P>
- * <P>
- * vCard 2.1 specifies:<BR />
- * word = &lt;any printable 7bit us-ascii except []=:., &gt;
- * </P>
- */
- public static boolean isV21Word(final String value) {
- if (TextUtils.isEmpty(value)) {
- return true;
- }
- final int asciiFirst = 0x20;
- final int asciiLast = 0x7E; // included
- final int length = value.length();
- for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
- final int c = value.codePointAt(i);
- if (!(asciiFirst <= c && c <= asciiLast) ||
- sUnAcceptableAsciiInV21WordSet.contains((char)c)) {
- return false;
- }
- }
- return true;
- }
-
- public static String toHalfWidthString(final String orgString) {
- if (TextUtils.isEmpty(orgString)) {
- return null;
- }
- final StringBuilder builder = new StringBuilder();
- final int length = orgString.length();
- for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) {
- // All Japanese character is able to be expressed by char.
- // Do not need to use String#codepPointAt().
- final char ch = orgString.charAt(i);
- final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch);
- if (halfWidthText != null) {
- builder.append(halfWidthText);
- } else {
- builder.append(ch);
- }
- }
- return builder.toString();
- }
-
- /**
- * Guesses the format of input image. Currently just the first few bytes are used.
- * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when
- * the guess failed.
- * @param input Image as byte array.
- * @return The image type or null when the type cannot be determined.
- */
- public static String guessImageType(final byte[] input) {
- if (input == null) {
- return null;
- }
- if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') {
- return "GIF";
- } else if (input.length >= 4 && input[0] == (byte) 0x89
- && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') {
- // Note: vCard 2.1 officially does not support PNG, but we may have it and
- // using X- word like "X-PNG" may not let importers know it is PNG.
- // So we use the String "PNG" as is...
- return "PNG";
- } else if (input.length >= 2 && input[0] == (byte) 0xff
- && input[1] == (byte) 0xd8) {
- return "JPEG";
- } else {
- return null;
- }
- }
-
- /**
- * @return True when all the given values are null or empty Strings.
- */
- public static boolean areAllEmpty(final String...values) {
- if (values == null) {
- return true;
- }
-
- for (final String value : values) {
- if (!TextUtils.isEmpty(value)) {
- return false;
- }
- }
- return true;
- }
-
- private VCardUtils() {
- }
-}
diff --git a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java
deleted file mode 100644
index e72c7df..0000000
--- a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard.exception;
-
-public class VCardAgentNotSupportedException extends VCardNotSupportedException {
- public VCardAgentNotSupportedException() {
- super();
- }
-
- public VCardAgentNotSupportedException(String message) {
- super(message);
- }
-
-} \ No newline at end of file
diff --git a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java
deleted file mode 100644
index 67db62c..0000000
--- a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.pim.vcard.exception;
-
-/**
- * Thrown when the vCard has some line starting with '#'. In the specification,
- * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit
- * such lines.
- */
-public class VCardInvalidCommentLineException extends VCardInvalidLineException {
- public VCardInvalidCommentLineException() {
- super();
- }
-
- public VCardInvalidCommentLineException(final String message) {
- super(message);
- }
-}
diff --git a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java
deleted file mode 100644
index 330153e..0000000
--- a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.pim.vcard.exception;
-
-/**
- * Thrown when the vCard has some line starting with '#'. In the specification,
- * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit
- * such lines.
- */
-public class VCardInvalidLineException extends VCardException {
- public VCardInvalidLineException() {
- super();
- }
-
- public VCardInvalidLineException(final String message) {
- super(message);
- }
-}
diff --git a/core/java/android/pim/vcard/exception/VCardNestedException.java b/core/java/android/pim/vcard/exception/VCardNestedException.java
deleted file mode 100644
index 503c2fb..0000000
--- a/core/java/android/pim/vcard/exception/VCardNestedException.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.pim.vcard.exception;
-
-/**
- * VCardException thrown when VCard is nested without VCardParser's being notified.
- */
-public class VCardNestedException extends VCardNotSupportedException {
- public VCardNestedException() {
- super();
- }
- public VCardNestedException(String message) {
- super(message);
- }
-}
diff --git a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java
deleted file mode 100644
index 616aa77..0000000
--- a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.pim.vcard.exception;
-
-/**
- * The exception which tells that the input VCard is probably valid from the view of
- * specification but not supported in the current framework for now.
- *
- * This is a kind of a good news from the view of development.
- * It may be good to ask users to send a report with the VCard example
- * for the future development.
- */
-public class VCardNotSupportedException extends VCardException {
- public VCardNotSupportedException() {
- super();
- }
- public VCardNotSupportedException(String message) {
- super(message);
- }
-} \ No newline at end of file
diff --git a/core/java/android/pim/vcard/exception/package.html b/core/java/android/pim/vcard/exception/package.html
deleted file mode 100644
index 26b8a32..0000000
--- a/core/java/android/pim/vcard/exception/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<HTML>
-<BODY>
-{@hide}
-</BODY>
-</HTML> \ No newline at end of file
diff --git a/core/java/android/pim/vcard/package.html b/core/java/android/pim/vcard/package.html
deleted file mode 100644
index 26b8a32..0000000
--- a/core/java/android/pim/vcard/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<HTML>
-<BODY>
-{@hide}
-</BODY>
-</HTML> \ No newline at end of file
diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java
new file mode 100644
index 0000000..42d555c
--- /dev/null
+++ b/core/java/android/preference/MultiSelectListPreference.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link Preference} that displays a list of entries as
+ * a dialog.
+ * <p>
+ * This preference will store a set of strings into the SharedPreferences.
+ * This set will contain one or more values from the
+ * {@link #setEntryValues(CharSequence[])} array.
+ *
+ * @attr ref android.R.styleable#MultiSelectListPreference_entries
+ * @attr ref android.R.styleable#MultiSelectListPreference_entryValues
+ */
+public class MultiSelectListPreference extends DialogPreference {
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+ private Set<String> mValues = new HashSet<String>();
+ private Set<String> mNewValues = new HashSet<String>();
+ private boolean mPreferenceChanged;
+
+ public MultiSelectListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.MultiSelectListPreference, 0, 0);
+ mEntries = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entries);
+ mEntryValues = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entryValues);
+ a.recycle();
+ }
+
+ public MultiSelectListPreference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Sets the human-readable entries to be shown in the list. This will be
+ * shown in subsequent dialogs.
+ * <p>
+ * Each entry must have a corresponding index in
+ * {@link #setEntryValues(CharSequence[])}.
+ *
+ * @param entries The entries.
+ * @see #setEntryValues(CharSequence[])
+ */
+ public void setEntries(CharSequence[] entries) {
+ mEntries = entries;
+ }
+
+ /**
+ * @see #setEntries(CharSequence[])
+ * @param entriesResId The entries array as a resource.
+ */
+ public void setEntries(int entriesResId) {
+ setEntries(getContext().getResources().getTextArray(entriesResId));
+ }
+
+ /**
+ * The list of entries to be shown in the list in subsequent dialogs.
+ *
+ * @return The list as an array.
+ */
+ public CharSequence[] getEntries() {
+ return mEntries;
+ }
+
+ /**
+ * The array to find the value to save for a preference when an entry from
+ * entries is selected. If a user clicks on the second item in entries, the
+ * second item in this array will be saved to the preference.
+ *
+ * @param entryValues The array to be used as values to save for the preference.
+ */
+ public void setEntryValues(CharSequence[] entryValues) {
+ mEntryValues = entryValues;
+ }
+
+ /**
+ * @see #setEntryValues(CharSequence[])
+ * @param entryValuesResId The entry values array as a resource.
+ */
+ public void setEntryValues(int entryValuesResId) {
+ setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
+ }
+
+ /**
+ * Returns the array of values to be saved for the preference.
+ *
+ * @return The array of values.
+ */
+ public CharSequence[] getEntryValues() {
+ return mEntryValues;
+ }
+
+ /**
+ * Sets the value of the key. This should contain entries in
+ * {@link #getEntryValues()}.
+ *
+ * @param values The values to set for the key.
+ */
+ public void setValues(Set<String> values) {
+ mValues = values;
+
+ persistStringSet(values);
+ }
+
+ /**
+ * Retrieves the current value of the key.
+ */
+ public Set<String> getValues() {
+ return mValues;
+ }
+
+ /**
+ * Returns the index of the given value (in the entry values array).
+ *
+ * @param value The value whose index should be returned.
+ * @return The index of the value, or -1 if not found.
+ */
+ public int findIndexOfValue(String value) {
+ if (value != null && mEntryValues != null) {
+ for (int i = mEntryValues.length - 1; i >= 0; i--) {
+ if (mEntryValues[i].equals(value)) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+
+ if (mEntries == null || mEntryValues == null) {
+ throw new IllegalStateException(
+ "MultiSelectListPreference requires an entries array and " +
+ "an entryValues array.");
+ }
+
+ boolean[] checkedItems = getSelectedItems();
+ builder.setMultiChoiceItems(mEntries, checkedItems,
+ new DialogInterface.OnMultiChoiceClickListener() {
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ if (isChecked) {
+ mPreferenceChanged |= mNewValues.add(mEntries[which].toString());
+ } else {
+ mPreferenceChanged |= mNewValues.remove(mEntries[which].toString());
+ }
+ }
+ });
+ mNewValues.clear();
+ mNewValues.addAll(mValues);
+ }
+
+ private boolean[] getSelectedItems() {
+ final CharSequence[] entries = mEntries;
+ final int entryCount = entries.length;
+ final Set<String> values = mValues;
+ boolean[] result = new boolean[entryCount];
+
+ for (int i = 0; i < entryCount; i++) {
+ result[i] = values.contains(entries[i].toString());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult && mPreferenceChanged) {
+ final Set<String> values = mNewValues;
+ if (callChangeListener(values)) {
+ setValues(values);
+ }
+ }
+ mPreferenceChanged = false;
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ final CharSequence[] defaultValues = a.getTextArray(index);
+ final int valueCount = defaultValues.length;
+ final Set<String> result = new HashSet<String>();
+
+ for (int i = 0; i < valueCount; i++) {
+ result.add(defaultValues[i].toString());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setValues(restoreValue ? getPersistedStringSet(mValues) : (Set<String>) defaultValue);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.values = getValues();
+ return myState;
+ }
+
+ private static class SavedState extends BaseSavedState {
+ Set<String> values;
+
+ public SavedState(Parcel source) {
+ super(source);
+ values = new HashSet<String>();
+ String[] strings = source.readStringArray();
+
+ final int stringCount = strings.length;
+ for (int i = 0; i < stringCount; i++) {
+ values.add(strings[i]);
+ }
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeStringArray(values.toArray(new String[0]));
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java
index 197d976..381f794 100644
--- a/core/java/android/preference/Preference.java
+++ b/core/java/android/preference/Preference.java
@@ -16,8 +16,7 @@
package android.preference;
-import java.util.ArrayList;
-import java.util.List;
+import com.android.internal.util.CharSequences;
import android.content.Context;
import android.content.Intent;
@@ -28,7 +27,6 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
-import com.android.internal.util.CharSequences;
import android.view.AbsSavedState;
import android.view.LayoutInflater;
import android.view.View;
@@ -36,6 +34,10 @@ import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
/**
* Represents the basic Preference UI building
* block displayed by a {@link PreferenceActivity} in the form of a
@@ -1250,6 +1252,61 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis
}
/**
+ * Attempts to persist a set of Strings to the {@link android.content.SharedPreferences}.
+ * <p>
+ * This will check if this Preference is persistent, get an editor from
+ * the {@link PreferenceManager}, put in the strings, and check if we should commit (and
+ * commit if so).
+ *
+ * @param values The values to persist.
+ * @return True if the Preference is persistent. (This is not whether the
+ * value was persisted, since we may not necessarily commit if there
+ * will be a batch commit later.)
+ * @see #getPersistedString(Set)
+ *
+ * @hide Pending API approval
+ */
+ protected boolean persistStringSet(Set<String> values) {
+ if (shouldPersist()) {
+ // Shouldn't store null
+ if (values.equals(getPersistedStringSet(null))) {
+ // It's already there, so the same as persisting
+ return true;
+ }
+
+ SharedPreferences.Editor editor = mPreferenceManager.getEditor();
+ editor.putStringSet(mKey, values);
+ tryCommit(editor);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to get a persisted set of Strings from the
+ * {@link android.content.SharedPreferences}.
+ * <p>
+ * This will check if this Preference is persistent, get the SharedPreferences
+ * from the {@link PreferenceManager}, and get the value.
+ *
+ * @param defaultReturnValue The default value to return if either the
+ * Preference is not persistent or the Preference is not in the
+ * shared preferences.
+ * @return The value from the SharedPreferences or the default return
+ * value.
+ * @see #persistStringSet(Set)
+ *
+ * @hide Pending API approval
+ */
+ protected Set<String> getPersistedStringSet(Set<String> defaultReturnValue) {
+ if (!shouldPersist()) {
+ return defaultReturnValue;
+ }
+
+ return mPreferenceManager.getSharedPreferences().getStringSet(mKey, defaultReturnValue);
+ }
+
+ /**
* Attempts to persist an int to the {@link android.content.SharedPreferences}.
*
* @param value The value to persist.
diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java
index 726793d..4686978 100644
--- a/core/java/android/preference/PreferenceActivity.java
+++ b/core/java/android/preference/PreferenceActivity.java
@@ -23,7 +23,10 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
+import android.text.TextUtils;
import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
/**
* Shows a hierarchy of {@link Preference} objects as
@@ -69,30 +72,43 @@ import android.view.View;
* As a convenience, this activity implements a click listener for any
* preference in the current hierarchy, see
* {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}.
- *
+ *
* @see Preference
* @see PreferenceScreen
*/
public abstract class PreferenceActivity extends ListActivity implements
PreferenceManager.OnPreferenceTreeClickListener {
-
+
private static final String PREFERENCES_TAG = "android:preferences";
-
+
+ // extras that allow any preference activity to be launched as part of a wizard
+
+ // show Back and Next buttons? takes boolean parameter
+ // Back will then return RESULT_CANCELED and Next RESULT_OK
+ private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar";
+
+ // specify custom text for the Back or Next buttons, or cause a button to not appear
+ // at all by setting it to null
+ private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text";
+ private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text";
+
+ private Button mNextButton;
+
private PreferenceManager mPreferenceManager;
-
+
private Bundle mSavedInstanceState;
/**
* The starting request code given out to preference framework.
*/
private static final int FIRST_REQUEST_CODE = 100;
-
+
private static final int MSG_BIND_PREFERENCES = 0;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
-
+
case MSG_BIND_PREFERENCES:
bindPreferences();
break;
@@ -105,7 +121,49 @@ public abstract class PreferenceActivity extends ListActivity implements
super.onCreate(savedInstanceState);
setContentView(com.android.internal.R.layout.preference_list_content);
-
+
+ // see if we should show Back/Next buttons
+ Intent intent = getIntent();
+ if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) {
+
+ findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE);
+
+ Button backButton = (Button)findViewById(com.android.internal.R.id.back_button);
+ backButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ mNextButton = (Button)findViewById(com.android.internal.R.id.next_button);
+ mNextButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_OK);
+ finish();
+ }
+ });
+
+ // set our various button parameters
+ if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) {
+ String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT);
+ if (TextUtils.isEmpty(buttonText)) {
+ mNextButton.setVisibility(View.GONE);
+ }
+ else {
+ mNextButton.setText(buttonText);
+ }
+ }
+ if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) {
+ String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT);
+ if (TextUtils.isEmpty(buttonText)) {
+ backButton.setVisibility(View.GONE);
+ }
+ else {
+ backButton.setText(buttonText);
+ }
+ }
+ }
+
mPreferenceManager = onCreatePreferenceManager();
getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
}
@@ -113,14 +171,13 @@ public abstract class PreferenceActivity extends ListActivity implements
@Override
protected void onStop() {
super.onStop();
-
+
mPreferenceManager.dispatchActivityStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
-
mPreferenceManager.dispatchActivityDestroy();
}
@@ -156,7 +213,7 @@ public abstract class PreferenceActivity extends ListActivity implements
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
-
+
mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data);
}
@@ -176,7 +233,7 @@ public abstract class PreferenceActivity extends ListActivity implements
if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
}
-
+
private void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
@@ -187,10 +244,10 @@ public abstract class PreferenceActivity extends ListActivity implements
}
}
}
-
+
/**
* Creates the {@link PreferenceManager}.
- *
+ *
* @return The {@link PreferenceManager} used by this activity.
*/
private PreferenceManager onCreatePreferenceManager() {
@@ -198,7 +255,7 @@ public abstract class PreferenceActivity extends ListActivity implements
preferenceManager.setOnPreferenceTreeClickListener(this);
return preferenceManager;
}
-
+
/**
* Returns the {@link PreferenceManager} used by this activity.
* @return The {@link PreferenceManager}.
@@ -206,7 +263,7 @@ public abstract class PreferenceActivity extends ListActivity implements
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
-
+
private void requirePreferenceManager() {
if (mPreferenceManager == null) {
throw new RuntimeException("This should be called after super.onCreate.");
@@ -215,7 +272,7 @@ public abstract class PreferenceActivity extends ListActivity implements
/**
* Sets the root of the preference hierarchy that this activity is showing.
- *
+ *
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*/
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
@@ -228,37 +285,37 @@ public abstract class PreferenceActivity extends ListActivity implements
}
}
}
-
+
/**
* Gets the root of the preference hierarchy that this activity is showing.
- *
+ *
* @return The {@link PreferenceScreen} that is the root of the preference
* hierarchy.
*/
public PreferenceScreen getPreferenceScreen() {
return mPreferenceManager.getPreferenceScreen();
}
-
+
/**
* Adds preferences from activities that match the given {@link Intent}.
- *
+ *
* @param intent The {@link Intent} to query activities.
*/
public void addPreferencesFromIntent(Intent intent) {
requirePreferenceManager();
-
+
setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen()));
}
-
+
/**
* Inflates the given XML resource and adds the preference hierarchy to the current
* preference hierarchy.
- *
+ *
* @param preferencesResId The XML resource ID to inflate.
*/
public void addPreferencesFromResource(int preferencesResId) {
requirePreferenceManager();
-
+
setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId,
getPreferenceScreen()));
}
@@ -269,20 +326,20 @@ public abstract class PreferenceActivity extends ListActivity implements
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
return false;
}
-
+
/**
* Finds a {@link Preference} based on its key.
- *
+ *
* @param key The key of the preference to retrieve.
* @return The {@link Preference} with the key, or null.
* @see PreferenceGroup#findPreference(CharSequence)
*/
public Preference findPreference(CharSequence key) {
-
+
if (mPreferenceManager == null) {
return null;
}
-
+
return mPreferenceManager.findPreference(key);
}
@@ -292,5 +349,14 @@ public abstract class PreferenceActivity extends ListActivity implements
mPreferenceManager.dispatchNewIntent(intent);
}
}
-
+
+ // give subclasses access to the Next button
+ /** @hide */
+ protected boolean hasNextButton() {
+ return mNextButton != null;
+ }
+ /** @hide */
+ protected Button getNextButton() {
+ return mNextButton;
+ }
}
diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java
index 9a09805..a23a5a7 100644
--- a/core/java/android/provider/Calendar.java
+++ b/core/java/android/provider/Calendar.java
@@ -76,64 +76,28 @@ public final class Calendar {
*/
public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter";
+
/**
- * Columns from the Calendars table that other tables join into themselves.
+ * Generic columns for use by sync adapters. The specific functions of
+ * these columns are private to the sync adapter. Other clients of the API
+ * should not attempt to either read or write this column.
*/
- public interface CalendarsColumns
- {
- /**
- * The color of the calendar
- * <P>Type: INTEGER (color value)</P>
- */
- public static final String COLOR = "color";
-
- /**
- * The level of access that the user has for the calendar
- * <P>Type: INTEGER (one of the values below)</P>
- */
- public static final String ACCESS_LEVEL = "access_level";
-
- /** Cannot access the calendar */
- public static final int NO_ACCESS = 0;
- /** Can only see free/busy information about the calendar */
- public static final int FREEBUSY_ACCESS = 100;
- /** Can read all event details */
- public static final int READ_ACCESS = 200;
- public static final int RESPOND_ACCESS = 300;
- public static final int OVERRIDE_ACCESS = 400;
- /** Full access to modify the calendar, but not the access control settings */
- public static final int CONTRIBUTOR_ACCESS = 500;
- public static final int EDITOR_ACCESS = 600;
- /** Full access to the calendar */
- public static final int OWNER_ACCESS = 700;
- /** Domain admin */
- public static final int ROOT_ACCESS = 800;
-
- /**
- * Is the calendar selected to be displayed?
- * <P>Type: INTEGER (boolean)</P>
- */
- public static final String SELECTED = "selected";
-
- /**
- * The timezone the calendar's events occurs in
- * <P>Type: TEXT</P>
- */
- public static final String TIMEZONE = "timezone";
-
- /**
- * If this calendar is in the list of calendars that are selected for
- * syncing then "sync_events" is 1, otherwise 0.
- * <p>Type: INTEGER (boolean)</p>
- */
- public static final String SYNC_EVENTS = "sync_events";
-
- /**
- * Sync state data.
- * <p>Type: String (blob)</p>
- */
- public static final String SYNC_STATE = "sync_state";
+ protected interface BaseSyncColumns {
+
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC1 = "sync1";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC2 = "sync2";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC3 = "sync3";
+ /** Generic column for use by sync adapters. */
+ public static final String SYNC4 = "sync4";
+ }
+ /**
+ * Columns for Sync information used by Calendars and Events tables.
+ */
+ public interface SyncColumns extends BaseSyncColumns {
/**
* The account that was used to sync the entry to the device.
* <P>Type: TEXT</P>
@@ -185,6 +149,12 @@ public final class Calendar {
*/
public static final String _SYNC_DIRTY = "_sync_dirty";
+ }
+
+ /**
+ * Columns from the Account information used by Calendars and Events tables.
+ */
+ public interface AccountColumns {
/**
* The name of the account instance to which this row belongs, which when paired with
* {@link #ACCOUNT_TYPE} identifies a specific account.
@@ -201,9 +171,159 @@ public final class Calendar {
}
/**
+ * Columns from the Calendars table that other tables join into themselves.
+ */
+ public interface CalendarsColumns {
+ /**
+ * The color of the calendar
+ * <P>Type: INTEGER (color value)</P>
+ */
+ public static final String COLOR = "color";
+
+ /**
+ * The level of access that the user has for the calendar
+ * <P>Type: INTEGER (one of the values below)</P>
+ */
+ public static final String ACCESS_LEVEL = "access_level";
+
+ /** Cannot access the calendar */
+ public static final int NO_ACCESS = 0;
+ /** Can only see free/busy information about the calendar */
+ public static final int FREEBUSY_ACCESS = 100;
+ /** Can read all event details */
+ public static final int READ_ACCESS = 200;
+ public static final int RESPOND_ACCESS = 300;
+ public static final int OVERRIDE_ACCESS = 400;
+ /** Full access to modify the calendar, but not the access control settings */
+ public static final int CONTRIBUTOR_ACCESS = 500;
+ public static final int EDITOR_ACCESS = 600;
+ /** Full access to the calendar */
+ public static final int OWNER_ACCESS = 700;
+ /** Domain admin */
+ public static final int ROOT_ACCESS = 800;
+
+ /**
+ * Is the calendar selected to be displayed?
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String SELECTED = "selected";
+
+ /**
+ * The timezone the calendar's events occurs in
+ * <P>Type: TEXT</P>
+ */
+ public static final String TIMEZONE = "timezone";
+
+ /**
+ * If this calendar is in the list of calendars that are selected for
+ * syncing then "sync_events" is 1, otherwise 0.
+ * <p>Type: INTEGER (boolean)</p>
+ */
+ public static final String SYNC_EVENTS = "sync_events";
+
+ /**
+ * Sync state data.
+ * <p>Type: String (blob)</p>
+ */
+ public static final String SYNC_STATE = "sync_state";
+
+ /**
+ * Whether the row has been deleted. A deleted row should be ignored.
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String DELETED = "deleted";
+ }
+
+ /**
+ * Class that represents a Calendar Entity. There is one entry per calendar.
+ */
+ public static class CalendarsEntity implements BaseColumns, SyncColumns, CalendarsColumns {
+
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY +
+ "/calendar_entities");
+
+ public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) {
+ return new EntityIteratorImpl(cursor, resolver);
+ }
+
+ public static EntityIterator newEntityIterator(Cursor cursor,
+ ContentProviderClient provider) {
+ return new EntityIteratorImpl(cursor, provider);
+ }
+
+ private static class EntityIteratorImpl extends CursorEntityIterator {
+ private final ContentResolver mResolver;
+ private final ContentProviderClient mProvider;
+
+ public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) {
+ super(cursor);
+ mResolver = resolver;
+ mProvider = null;
+ }
+
+ public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) {
+ super(cursor);
+ mResolver = null;
+ mProvider = provider;
+ }
+
+ @Override
+ public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException {
+ // we expect the cursor is already at the row we need to read from
+ final long calendarId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
+
+ // Create the content value
+ ContentValues cv = new ContentValues();
+ cv.put(_ID, calendarId);
+
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT_TYPE);
+
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_TIME);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA);
+ DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY);
+ DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_MARK);
+
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC2);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC3);
+
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.NAME);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv,
+ Calendars.DISPLAY_NAME);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.HIDDEN);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.COLOR);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELECTED);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SYNC_EVENTS);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.LOCATION);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TIMEZONE);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv,
+ Calendars.OWNER_ACCOUNT);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv,
+ Calendars.ORGANIZER_CAN_RESPOND);
+
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED);
+
+ // Create the Entity from the ContentValue
+ Entity entity = new Entity(cv);
+
+ // Set cursor to next row
+ cursor.moveToNext();
+
+ // Return the created Entity
+ return entity;
+ }
+ }
+ }
+
+ /**
* Contains a list of available calendars.
*/
- public static class Calendars implements BaseColumns, CalendarsColumns
+ public static class Calendars implements BaseColumns, SyncColumns, AccountColumns,
+ CalendarsColumns
{
private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?"
+ " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
@@ -258,6 +378,24 @@ public final class Calendar {
public static final String URL = "url";
/**
+ * The URL for the calendar itself
+ * <P>Type: TEXT (URL)</P>
+ */
+ public static final String SELF_URL = "selfUrl";
+
+ /**
+ * The URL for the calendar to be edited
+ * <P>Type: TEXT (URL)</P>
+ */
+ public static final String EDIT_URL = "editUrl";
+
+ /**
+ * The URL for the calendar events
+ * <P>Type: TEXT (URL)</P>
+ */
+ public static final String EVENTS_URL = "eventsUrl";
+
+ /**
* The name of the calendar
* <P>Type: TEXT</P>
*/
@@ -296,6 +434,9 @@ public final class Calendar {
public static final String ORGANIZER_CAN_RESPOND = "organizerCanRespond";
}
+ /**
+ * Columns from the Attendees table that other tables join into themselves.
+ */
public interface AttendeesColumns {
/**
@@ -361,8 +502,7 @@ public final class Calendar {
/**
* Columns from the Events table that other tables join into themselves.
*/
- public interface EventsColumns
- {
+ public interface EventsColumns {
/**
* The calendar the event belongs to
* <P>Type: INTEGER (foreign key to the Calendars table)</P>
@@ -438,6 +578,18 @@ public final class Calendar {
public static final String DTEND = "dtend";
/**
+ * The time the event starts with allDay events in a local tz
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String DTSTART2 = "dtstart2";
+
+ /**
+ * The time the event ends with allDay events in a local tz
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String DTEND2 = "dtend2";
+
+ /**
* The duration of the event
* <P>Type: TEXT (duration in RFC2445 format)</P>
*/
@@ -450,6 +602,12 @@ public final class Calendar {
public static final String EVENT_TIMEZONE = "eventTimezone";
/**
+ * The timezone for the event, allDay events will have a local tz instead of UTC
+ * <P>Type: TEXT
+ */
+ public static final String EVENT_TIMEZONE2 = "eventTimezone2";
+
+ /**
* Whether the event lasts all day or not
* <P>Type: INTEGER (boolean)</P>
*/
@@ -598,7 +756,8 @@ public final class Calendar {
/**
* Contains one entry per calendar event. Recurring events show up as a single entry.
*/
- public static final class EventsEntity implements BaseColumns, EventsColumns, CalendarsColumns {
+ public static final class EventsEntity implements BaseColumns, SyncColumns, AccountColumns,
+ EventsColumns {
/**
* The content:// style URL for this table
*/
@@ -703,8 +862,8 @@ public final class Calendar {
DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA);
DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY);
DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION);
- DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED);
- DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.URL);
+ DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, EventsColumns.DELETED);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1);
Entity entity = new Entity(cv);
Cursor subCursor;
@@ -795,7 +954,8 @@ public final class Calendar {
/**
* Contains one entry per calendar event. Recurring events show up as a single entry.
*/
- public static final class Events implements BaseColumns, EventsColumns, CalendarsColumns {
+ public static final class Events implements BaseColumns, SyncColumns, AccountColumns,
+ EventsColumns {
private static final String[] FETCH_ENTRY_COLUMNS =
new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID };
@@ -981,7 +1141,7 @@ public final class Calendar {
public static final String MAX_EVENTDAYS = "maxEventDays";
}
- public static final class CalendarMetaData implements CalendarMetaDataColumns {
+ public static final class CalendarMetaData implements CalendarMetaDataColumns, BaseColumns {
}
public interface EventDaysColumns {
@@ -1379,4 +1539,43 @@ public final class Calendar {
public static final Uri CONTENT_URI =
Uri.withAppendedPath(Calendar.CONTENT_URI, CONTENT_DIRECTORY);
}
+
+ /**
+ * Columns from the EventsRawTimes table
+ */
+ public interface EventsRawTimesColumns {
+ /**
+ * The corresponding event id
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The RFC2445 compliant time the event starts
+ * <P>Type: TEXT</P>
+ */
+ public static final String DTSTART_2445 = "dtstart2445";
+
+ /**
+ * The RFC2445 compliant time the event ends
+ * <P>Type: TEXT</P>
+ */
+ public static final String DTEND_2445 = "dtend2445";
+
+ /**
+ * The RFC2445 compliant original instance time of the recurring event for which this
+ * event is an exception.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445";
+
+ /**
+ * The RFC2445 compliant last date this event repeats on, or NULL if it never ends
+ * <P>Type: TEXT</P>
+ */
+ public static final String LAST_DATE_2445 = "lastDate2445";
+ }
+
+ public static final class EventsRawTimes implements BaseColumns, EventsRawTimesColumns {
+ }
}
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 40a408a..218f21c 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -130,6 +130,17 @@ public final class ContactsContract {
public static final String REQUESTING_PACKAGE_PARAM_KEY = "requesting_package";
/**
+ * Query parameter that should be used by the client to access a specific
+ * {@link Directory}. The parameter value should be the _ID of the corresponding
+ * directory, e.g.
+ * {@code content://com.android.contacts/data/emails/filter/acme?directory=3}
+ *
+ * @hide
+ */
+ public static final String DIRECTORY_PARAM_KEY = "directory";
+
+
+ /**
* @hide
*/
public static final class Preferences {
@@ -181,6 +192,227 @@ public final class ContactsContract {
}
/**
+ * A Directory represents a contacts corpus, e.g. Local contacts,
+ * Google Apps Global Address List or Corporate Global Address List.
+ * <p>
+ * A Directory is implemented as a content provider with its unique authority and
+ * the same API as the main Contacts Provider. However, there is no expectation that
+ * every directory provider will implement this Contract in its entirety. If a
+ * directory provider does not have an implementation for a specific request, it
+ * should throw an UnsupportedOperationException.
+ * </p>
+ * <p>
+ * The most important use case for Directories is search. A Directory provider is
+ * expected to support at least {@link Contacts#CONTENT_FILTER_URI
+ * Contacts#CONTENT_FILTER_URI}. If a Directory provider wants to participate
+ * in email and phone lookup functionalities, it should also implement
+ * {@link CommonDataKinds.Email#CONTENT_FILTER_URI CommonDataKinds.Email.CONTENT_FILTER_URI}
+ * and
+ * {@link CommonDataKinds.Phone#CONTENT_FILTER_URI CommonDataKinds.Phone.CONTENT_FILTER_URI}.
+ * </p>
+ * <p>
+ * A directory provider should return NULL for every projection field it does not
+ * recognize, rather than throwing an exception. This way it will not be broken
+ * if ContactsContract is extended with new fields in the future.
+ * </p>
+ * <p>
+ * The client interacts with a directory via Contacts Provider by supplying an
+ * optional {@code directory=} query parameter.
+ * <p>
+ * <p>
+ * When the Contacts Provider receives the request, it transforms the URI and forwards
+ * the request to the corresponding directory content provider.
+ * The URI is transformed in the following fashion:
+ * <ul>
+ * <li>The URI authority is replaced with the corresponding {@link #DIRECTORY_AUTHORITY}.</li>
+ * <li>The {@code accountName=} and {@code accountType=} parameters are added or
+ * replaced using the corresponding {@link #ACCOUNT_TYPE} and {@link #ACCOUNT_NAME} values.</li>
+ * <li>If the URI is missing a {@link ContactsContract#REQUESTING_PACKAGE_PARAM_KEY}
+ * parameter, this parameter is added.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Clients should send directory requests to Contacts Provider and let it
+ * forward them to the respective providers rather than constructing directory provider
+ * URIs by themselves. This level of indirection allows Contacts Provider to
+ * implement additional system-level features and optimizations.
+ * Also, directory providers may reject requests coming from other
+ * clients than the Contacts Provider itself.
+ * </p>
+ * <p>
+ * The Directory table always has at least these two rows:
+ * <ul>
+ * <li>
+ * The local directory. It has {@link Directory#_ID Directory._ID} =
+ * {@link Directory#DEFAULT Directory.DEFAULT}. This directory can be used to access locally
+ * stored contacts. The same can be achieved by omitting the {@code directory=}
+ * parameter altogether.
+ * </li>
+ * <li>
+ * The local invisible contacts. The corresponding directory ID is
+ * {@link Directory#LOCAL_INVISIBLE Directory.LOCAL_INVISIBLE}.
+ * </li>
+ * </ul>
+ * </p>
+ * <p>
+ * Other directories should register themselves by explicitly adding rows to this table.
+ * </p>
+ * <p>
+ * When a row is inserted in this table, it is automatically associated with the package
+ * (apk) that made the request. If the package is later uninstalled, all directory rows
+ * it inserted are automatically removed.
+ * </p>
+ * <p>
+ * A directory row can be optionally associated with an account.
+ * If the account is later removed, the corresponding directory rows are
+ * automatically removed.
+ * </p>
+ *
+ * @hide
+ */
+ public static final class Directory implements BaseColumns {
+
+ /**
+ * Not instantiable.
+ */
+ private Directory() {
+ }
+
+ /**
+ * The content:// style URI for this table. Requests to this URI can be
+ * performed on the UI thread because they are always unblocking.
+ *
+ * @hide
+ */
+ public static final Uri CONTENT_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "directories");
+
+ /**
+ * The MIME-type of {@link #CONTENT_URI} providing a directory of
+ * contact directories.
+ *
+ * @hide
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/contact_directories";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} item.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/contact_directory";
+
+ /**
+ * The name of the package that owns this directory. This field is
+ * required in an insert request and must match the name of the package
+ * making the request. If the package is later uninstalled, the
+ * directories it owns are automatically removed from this table. Only
+ * the specified package is allowed to modify or delete this row later.
+ *
+ * <p>TYPE: TEXT</p>
+ *
+ * @hide
+ */
+ public static final String PACKAGE_NAME = "packageName";
+
+ /**
+ * The type of directory captured as a resource ID in the context of the
+ * package {@link #PACKAGE_NAME}, e.g. "Corporate Directory"
+ *
+ * <p>TYPE: INTEGER</p>
+ *
+ * @hide
+ */
+ public static final String TYPE_RESOURCE_ID = "typeResourceId";
+
+ /**
+ * An optional name that can be used in the UI to represent this directory,
+ * e.g. "Acme Corp"
+ * <p>TYPE: text</p>
+ *
+ * @hide
+ */
+ public static final String DISPLAY_NAME = "displayName";
+
+ /**
+ * The authority to which the request should forwarded in order to access
+ * this directory.
+ *
+ * <p>TYPE: text</p>
+ *
+ * @hide
+ */
+ public static final String DIRECTORY_AUTHORITY = "authority";
+
+ /**
+ * The account type which this directory is associated.
+ *
+ * <p>TYPE: text</p>
+ *
+ * @hide
+ */
+ public static final String ACCOUNT_TYPE = "accountType";
+
+ /**
+ * The account with which this directory is associated. If the account is later
+ * removed, the directories it owns are automatically removed from this table.
+ *
+ * <p>TYPE: text</p>
+ *
+ * @hide
+ */
+ public static final String ACCOUNT_NAME = "accountName";
+
+ /**
+ * One of {@link #EXPORT_SUPPORT_NONE}, {@link #EXPORT_SUPPORT_ANY_ACCOUNT},
+ * {@link #EXPORT_SUPPORT_SAME_ACCOUNT_ONLY}. This is the expectation the
+ * directory has for data exported from it. Clients must obey this setting.
+ *
+ * @hide
+ */
+ public static final String EXPORT_SUPPORT = "exportSupport";
+
+ /**
+ * An {@link #EXPORT_SUPPORT} setting that indicates that the directory
+ * does not allow any data to be copied out of it.
+ *
+ * @hide
+ */
+ public static final int EXPORT_SUPPORT_NONE = 0;
+
+ /**
+ * An {@link #EXPORT_SUPPORT} setting that indicates that the directory
+ * allow its data copied only to the account specified by
+ * {@link #ACCOUNT_TYPE}/{@link #ACCOUNT_NAME}.
+ *
+ * @hide
+ */
+ public static final int EXPORT_SUPPORT_SAME_ACCOUNT_ONLY = 1;
+
+ /**
+ * An {@link #EXPORT_SUPPORT} setting that indicates that the directory
+ * allow its data copied to any contacts account.
+ *
+ * @hide
+ */
+ public static final int EXPORT_SUPPORT_ANY_ACCOUNT = 2;
+
+ /**
+ * _ID of the default directory, which represents locally stored contacts.
+ *
+ * @hide
+ */
+ public static final long DEFAULT = 0;
+
+ /**
+ * _ID of the directory that represents locally stored invisible contacts.
+ *
+ * @hide
+ */
+ public static final long LOCAL_INVISIBLE = 1;
+ }
+
+ /**
* @hide should be removed when users are updated to refer to SyncState
* @deprecated use SyncState instead
*/
@@ -4902,6 +5134,23 @@ public final class ContactsContract {
* Type: INTEGER (boolean)
*/
public static final String SHOULD_SYNC = "should_sync";
+
+ /**
+ * Any newly created contacts will automatically be added to groups that have this
+ * flag set to true.
+ * <p>
+ * Type: INTEGER (boolean)
+ */
+ public static final String AUTO_ADD = "auto_add";
+
+ /**
+ * When a contacts is marked as a favorites it will be automatically added
+ * to the groups that have this flag set, and when it is removed from favorites
+ * it will be removed from these groups.
+ * <p>
+ * Type: INTEGER (boolean)
+ */
+ public static final String FAVORITES = "favorites";
}
/**
@@ -5042,6 +5291,8 @@ public final class ContactsContract {
DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, DELETED);
DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, NOTES);
DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SHOULD_SYNC);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, FAVORITES);
+ DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, AUTO_ADD);
cursor.moveToNext();
return new Entity(values);
}
@@ -5558,6 +5809,28 @@ public final class ContactsContract {
"com.android.contacts.action.SHOW_OR_CREATE_CONTACT";
/**
+ * Starts an Activity that lets the user select the multiple phones from a
+ * list of phone numbers which come from the contacts or
+ * {@link #EXTRA_PHONE_URIS}.
+ * <p>
+ * The phone numbers being passed in through {@link #EXTRA_PHONE_URIS}
+ * could belong to the contacts or not, and will be selected by default.
+ * <p>
+ * The user's selection will be returned from
+ * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)}
+ * if the resultCode is
+ * {@link android.app.Activity#RESULT_OK}, the array of picked phone
+ * numbers are in the Intent's
+ * {@link #EXTRA_PHONE_URIS}; otherwise, the
+ * {@link android.app.Activity#RESULT_CANCELED} is returned if the user
+ * left the Activity without changing the selection.
+ *
+ * @hide
+ */
+ public static final String ACTION_GET_MULTIPLE_PHONES =
+ "com.android.contacts.action.GET_MULTIPLE_PHONES";
+
+ /**
* Used with {@link #SHOW_OR_CREATE_CONTACT} to force creating a new
* contact if no matching contact found. Otherwise, default behavior is
* to prompt user with dialog before creating.
@@ -5578,6 +5851,23 @@ public final class ContactsContract {
"com.android.contacts.action.CREATE_DESCRIPTION";
/**
+ * Used with {@link #ACTION_GET_MULTIPLE_PHONES} as the input or output value.
+ * <p>
+ * The phone numbers want to be picked by default should be passed in as
+ * input value. These phone numbers could belong to the contacts or not.
+ * <p>
+ * The phone numbers which were picked by the user are returned as output
+ * value.
+ * <p>
+ * Type: array of URIs, the tel URI is used for the phone numbers which don't
+ * belong to any contact, the content URI is used for phone id in contacts.
+ *
+ * @hide
+ */
+ public static final String EXTRA_PHONE_URIS =
+ "com.android.contacts.extra.PHONE_URIS";
+
+ /**
* Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a
* dialog location using screen coordinates. When not specified, the
* dialog will be centered.
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 40ed980..293d31c 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -237,8 +237,67 @@ public final class MediaStore {
* <P>Type: TEXT</P>
*/
public static final String MIME_TYPE = "mime_type";
+
+ /**
+ * The MTP object handle of a newly transfered file.
+ * Used internally by the MediaScanner
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String MTP_OBJECT_HANDLE = "mtp_object_handle";
}
+
+
+ /**
+ * Media provider interface used by MTP implementation.
+ * @hide
+ */
+ public static final class MtpObjects {
+
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/object");
+ }
+
+ public static final Uri getContentUri(String volumeName,
+ long objectId) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ + "/object/" + objectId);
+ }
+
+ /**
+ * Fields for master table for all media files.
+ * Table also contains MediaColumns._ID, DATA, SIZE and DATE_MODIFIED.
+ */
+ public interface ObjectColumns extends MediaColumns {
+ /**
+ * The MTP format code of the file
+ * <P>Type: INTEGER</P>
+ */
+ public static final String FORMAT = "format";
+
+ /**
+ * The index of the parent directory of the file
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PARENT = "parent";
+
+ /**
+ * Identifier for the media table containing the object.
+ * Used internally by MediaProvider
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MEDIA_TABLE = "media_table";
+
+ /**
+ * The ID of the object in its media table.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MEDIA_ID = "media_id";
+ }
+ }
+
/**
* This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended
* to be accessed elsewhere.
@@ -317,22 +376,23 @@ public final class MediaStore {
// Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo);
// If the magic is non-zero, we simply return thumbnail if it does exist.
// querying MediaProvider and simply return thumbnail.
- MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri);
- long magic = thumbFile.getMagic(origId);
- if (magic != 0) {
- if (kind == MICRO_KIND) {
- byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB];
- if (thumbFile.getMiniThumbFromFile(origId, data) != null) {
- bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
- if (bitmap == null) {
- Log.w(TAG, "couldn't decode byte array.");
+ MiniThumbFile thumbFile = new MiniThumbFile(isVideo ? Video.Media.EXTERNAL_CONTENT_URI
+ : Images.Media.EXTERNAL_CONTENT_URI);
+ Cursor c = null;
+ try {
+ long magic = thumbFile.getMagic(origId);
+ if (magic != 0) {
+ if (kind == MICRO_KIND) {
+ byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB];
+ if (thumbFile.getMiniThumbFromFile(origId, data) != null) {
+ bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (bitmap == null) {
+ Log.w(TAG, "couldn't decode byte array.");
+ }
}
- }
- return bitmap;
- } else if (kind == MINI_KIND) {
- String column = isVideo ? "video_id=" : "image_id=";
- Cursor c = null;
- try {
+ return bitmap;
+ } else if (kind == MINI_KIND) {
+ String column = isVideo ? "video_id=" : "image_id=";
c = cr.query(baseUri, PROJECTION, column + origId, null, null);
if (c != null && c.moveToFirst()) {
bitmap = getMiniThumbFromFile(c, baseUri, cr, options);
@@ -340,17 +400,13 @@ public final class MediaStore {
return bitmap;
}
}
- } finally {
- if (c != null) c.close();
}
}
- }
- Cursor c = null;
- try {
Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1")
.appendQueryParameter("orig_id", String.valueOf(origId))
.appendQueryParameter("group_id", String.valueOf(groupId)).build();
+ if (c != null) c.close();
c = cr.query(blockingUri, PROJECTION, null, null, null);
// This happens when original image/video doesn't exist.
if (c == null) return null;
@@ -397,6 +453,9 @@ public final class MediaStore {
Log.w(TAG, ex);
} finally {
if (c != null) c.close();
+ // To avoid file descriptor leak in application process.
+ thumbFile.deactivate();
+ thumbFile = null;
}
return bitmap;
}
diff --git a/core/java/android/provider/Mtp.java b/core/java/android/provider/Mtp.java
new file mode 100644
index 0000000..15f8666
--- /dev/null
+++ b/core/java/android/provider/Mtp.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.content.ContentUris;
+import android.net.Uri;
+import android.util.Log;
+
+
+/**
+ * The MTP provider supports accessing content on MTP and PTP devices.
+ * @hide
+ */
+public final class Mtp
+{
+ private final static String TAG = "Mtp";
+
+ public static final String AUTHORITY = "mtp";
+
+ private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
+ private static final String CONTENT_AUTHORITY_DEVICE_SLASH = "content://" + AUTHORITY + "/device/";
+
+ /**
+ * Contains list of all MTP/PTP devices
+ */
+ public static final class Device implements BaseColumns {
+
+ public static final Uri CONTENT_URI = Uri.parse(CONTENT_AUTHORITY_SLASH + "device");
+
+ public static Uri getContentUri(int deviceID) {
+ return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID);
+ }
+
+ /**
+ * The manufacturer of the device
+ * <P>Type: TEXT</P>
+ */
+ public static final String MANUFACTURER = "manufacturer";
+
+ /**
+ * The model name of the device
+ * <P>Type: TEXT</P>
+ */
+ public static final String MODEL = "model";
+ }
+
+ /**
+ * Contains list of storage units for an MTP/PTP device
+ */
+ public static final class Storage implements BaseColumns {
+
+ public static Uri getContentUri(int deviceID) {
+ return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + "/storage");
+ }
+
+ public static Uri getContentUri(int deviceID, int storageID) {
+ return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID + "/storage/" + storageID);
+ }
+
+ /**
+ * Storage unit identifier
+ * <P>Type: TEXT</P>
+ */
+ public static final String IDENTIFIER = "identifier";
+
+ /**
+ * Storage unit description
+ * <P>Type: TEXT</P>
+ */
+ public static final String DESCRIPTION = "description";
+ }
+
+ /**
+ * Contains list of objects on an MTP/PTP device
+ */
+ public static final class Object implements BaseColumns {
+
+ public static Uri getContentUri(int deviceID, int objectID) {
+ return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID
+ + "/object/" + objectID);
+ }
+
+ public static Uri getContentUriForObjectChildren(int deviceID, int objectID) {
+ return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID
+ + "/object/" + objectID + "/child");
+ }
+
+ public static Uri getContentUriForStorageChildren(int deviceID, int storageID) {
+ return Uri.parse(CONTENT_AUTHORITY_DEVICE_SLASH + deviceID
+ + "/storage/" + storageID + "/child");
+ }
+
+ /**
+ * The following columns correspond to the fields in the ObjectInfo dataset
+ * as described in the MTP specification.
+ */
+
+ /**
+ * The ID of the storage unit containing the object.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String STORAGE_ID = "storage_id";
+
+ /**
+ * The object's format. Can be one of the FORMAT_* symbols below,
+ * or any of the valid MTP object formats as defined in the MTP specification.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String FORMAT = "format";
+
+ /**
+ * The protection status of the object. See the PROTECTION_STATUS_*symbols below.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PROTECTION_STATUS = "protection_status";
+
+ /**
+ * The size of the object in bytes.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SIZE = "size";
+
+ /**
+ * The object's thumbnail format. Can be one of the FORMAT_* symbols below,
+ * or any of the valid MTP object formats as defined in the MTP specification.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String THUMB_FORMAT = "format";
+
+ /**
+ * The size of the object's thumbnail in bytes.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String THUMB_SIZE = "thumb_size";
+
+ /**
+ * The width of the object's thumbnail in pixels.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String THUMB_WIDTH = "thumb_width";
+
+ /**
+ * The height of the object's thumbnail in pixels.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String THUMB_HEIGHT = "thumb_height";
+
+ /**
+ * The object's thumbnail.
+ * <P>Type: BLOB</P>
+ */
+ public static final String THUMB = "thumb";
+
+ /**
+ * The width of the object in pixels.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String IMAGE_WIDTH = "image_width";
+
+ /**
+ * The height of the object in pixels.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String IMAGE_HEIGHT = "image_height";
+
+ /**
+ * The depth of the object in bits per pixel.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String IMAGE_DEPTH = "image_depth";
+
+ /**
+ * The ID of the object's parent, or zero if the object
+ * is in the root of its storage unit.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PARENT = "parent";
+
+ /**
+ * The association type for a container object.
+ * For folders this is typically {@link #ASSOCIATION_TYPE_GENERIC_FOLDER}
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ASSOCIATION_TYPE = "association_type";
+
+ /**
+ * Contains additional information about container objects.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ASSOCIATION_DESC = "association_desc";
+
+ /**
+ * The sequence number of the object, typically used for an association
+ * containing images taken in sequence.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SEQUENCE_NUMBER = "sequence_number";
+
+ /**
+ * The name of the object.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The date the object was created, in seconds since January 1, 1970.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DATE_CREATED = "date_created";
+
+ /**
+ * The date the object was last modified, in seconds since January 1, 1970.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DATE_MODIFIED = "date_modified";
+
+ /**
+ * A list of keywords associated with an object, separated by spaces.
+ * <P>Type: TEXT</P>
+ */
+ public static final String KEYWORDS = "keywords";
+
+ /**
+ * Contants for {@link #FORMAT} and {@link #THUMB_FORMAT}
+ */
+ public static final int FORMAT_UNDEFINED = 0x3000;
+ public static final int FORMAT_ASSOCIATION = 0x3001;
+ public static final int FORMAT_SCRIPT = 0x3002;
+ public static final int FORMAT_EXECUTABLE = 0x3003;
+ public static final int FORMAT_TEXT = 0x3004;
+ public static final int FORMAT_HTML = 0x3005;
+ public static final int FORMAT_DPOF = 0x3006;
+ public static final int FORMAT_AIFF = 0x3007;
+ public static final int FORMAT_WAV = 0x3008;
+ public static final int FORMAT_MP3 = 0x3009;
+ public static final int FORMAT_AVI = 0x300A;
+ public static final int FORMAT_MPEG = 0x300B;
+ public static final int FORMAT_ASF = 0x300C;
+ public static final int FORMAT_DEFINED = 0x3800;
+ public static final int FORMAT_EXIF_JPEG = 0x3801;
+ public static final int FORMAT_TIFF_EP = 0x3802;
+ public static final int FORMAT_FLASHPIX = 0x3803;
+ public static final int FORMAT_BMP = 0x3804;
+ public static final int FORMAT_CIFF = 0x3805;
+ public static final int FORMAT_GIF = 0x3807;
+ public static final int FORMAT_JFIF = 0x3808;
+ public static final int FORMAT_CD = 0x3809;
+ public static final int FORMAT_PICT = 0x380A;
+ public static final int FORMAT_PNG = 0x380B;
+ public static final int FORMAT_TIFF = 0x380D;
+ public static final int FORMAT_TIFF_IT = 0x380E;
+ public static final int FORMAT_JP2 = 0x380F;
+ public static final int FORMAT_JPX = 0x3810;
+ public static final int FORMAT_UNDEFINED_FIRMWARE = 0xB802;
+ public static final int FORMAT_WINDOWS_IMAGE_FORMAT = 0xB881;
+ public static final int FORMAT_UNDEFINED_AUDIO = 0xB900;
+ public static final int FORMAT_WMA = 0xB901;
+ public static final int FORMAT_OGG = 0xB902;
+ public static final int FORMAT_AAC = 0xB903;
+ public static final int FORMAT_AUDIBLE = 0xB904;
+ public static final int FORMAT_FLAC = 0xB906;
+ public static final int FORMAT_UNDEFINED_VIDEO = 0xB980;
+ public static final int FORMAT_WMV = 0xB981;
+ public static final int FORMAT_MP4_CONTAINER = 0xB982;
+ public static final int FORMAT_MP2 = 0xB983;
+ public static final int FORMAT_3GP_CONTAINER = 0xB984;
+ public static final int FORMAT_UNDEFINED_COLLECTION = 0xBA00;
+ public static final int FORMAT_ABSTRACT_MULTIMEDIA_ALBUM = 0xBA01;
+ public static final int FORMAT_ABSTRACT_IMAGE_ALBUM = 0xBA02;
+ public static final int FORMAT_ABSTRACT_AUDIO_ALBUM = 0xBA03;
+ public static final int FORMAT_ABSTRACT_VIDEO_ALBUM = 0xBA04;
+ public static final int FORMAT_ABSTRACT_AV_PLAYLIST = 0xBA05;
+ public static final int FORMAT_ABSTRACT_CONTACT_GROUP = 0xBA06;
+ public static final int FORMAT_ABSTRACT_MESSAGE_FOLDER = 0xBA07;
+ public static final int FORMAT_ABSTRACT_CHAPTERED_PRODUCTION = 0xBA08;
+ public static final int FORMAT_ABSTRACT_AUDIO_PLAYLIST = 0xBA09;
+ public static final int FORMAT_ABSTRACT_VIDEO_PLAYLIST = 0xBA0A;
+ public static final int FORMAT_ABSTRACT_MEDIACAST = 0xBA0B;
+ public static final int FORMAT_WPL_PLAYLIST = 0xBA10;
+ public static final int FORMAT_M3U_PLAYLIST = 0xBA11;
+ public static final int FORMAT_MPL_PLAYLIST = 0xBA12;
+ public static final int FORMAT_ASX_PLAYLIST = 0xBA13;
+ public static final int FORMAT_PLS_PLAYLIST = 0xBA14;
+ public static final int FORMAT_UNDEFINED_DOCUMENT = 0xBA80;
+ public static final int FORMAT_ABSTRACT_DOCUMENT = 0xBA81;
+ public static final int FORMAT_XML_DOCUMENT = 0xBA82;
+ public static final int FORMAT_MS_WORD_DOCUMENT = 0xBA83;
+ public static final int FORMAT_MHT_COMPILED_HTML_DOCUMENT = 0xBA84;
+ public static final int FORMAT_MS_EXCEL_SPREADSHEET = 0xBA85;
+ public static final int FORMAT_MS_POWERPOINT_PRESENTATION = 0xBA86;
+ public static final int FORMAT_UNDEFINED_MESSAGE = 0xBB00;
+ public static final int FORMAT_ABSTRACT_MESSSAGE = 0xBB01;
+ public static final int FORMAT_UNDEFINED_CONTACT = 0xBB80;
+ public static final int FORMAT_ABSTRACT_CONTACT = 0xBB81;
+ public static final int FORMAT_VCARD_2 = 0xBB82;
+
+ /**
+ * Object is not protected. It may be modified and deleted, and its properties
+ * may be modified.
+ */
+ public static final int PROTECTION_STATUS_NONE = 0;
+
+ /**
+ * Object can not be modified or deleted and its properties can not be modified.
+ */
+ public static final int PROTECTION_STATUS_READ_ONLY = 0x8001;
+
+ /**
+ * Object can not be modified or deleted but its properties are modifiable.
+ */
+ public static final int PROTECTION_STATUS_READ_ONLY_DATA = 0x8002;
+
+ /**
+ * Object's contents can not be transfered from the device, but the object
+ * may be moved or deleted and its properties may be modified.
+ */
+ public static final int PROTECTION_STATUS_NON_TRANSFERABLE_DATA = 0x8003;
+
+ public static final int ASSOCIATION_TYPE_GENERIC_FOLDER = 0x0001;
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ea4738f..4ec5363 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -16,14 +16,11 @@
package android.provider;
-import com.google.android.collect.Maps;
-import org.apache.commons.codec.binary.Base64;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.content.ComponentName;
-import android.content.ContentQueryMap;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -38,19 +35,14 @@ import android.database.Cursor;
import android.database.SQLException;
import android.net.Uri;
import android.os.*;
-import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.AndroidException;
import android.util.Config;
import android.util.Log;
import java.net.URISyntaxException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Map;
/**
@@ -1009,7 +1001,7 @@ public final class Settings {
public static boolean hasInterestingConfigurationChanges(int changes) {
return (changes&ActivityInfo.CONFIG_FONT_SCALE) != 0;
}
-
+
public static boolean getShowGTalkServiceStatus(ContentResolver cr) {
return getInt(cr, SHOW_GTALK_SERVICE_STATUS, 0) != 0;
}
@@ -1216,7 +1208,7 @@ public final class Settings {
public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern";
/**
- * @deprecated Use
+ * @deprecated Use
* {@link android.provider.Settings.Secure#LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED}
* instead
*/
@@ -2290,6 +2282,14 @@ public final class Settings {
}
/**
+ * Get the key that retrieves a bluetooth Input Device's priority.
+ * @hide
+ */
+ public static final String getBluetoothInputDevicePriorityKey(String address) {
+ return ("bluetooth_input_device_priority_" + address.toUpperCase());
+ }
+
+ /**
* Whether or not data roaming is enabled. (0 = false, 1 = true)
*/
public static final String DATA_ROAMING = "data_roaming";
@@ -2416,6 +2416,14 @@ public final class Settings {
public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url";
/**
+ * A positive value indicates the frequency of SamplingProfiler
+ * taking snapshots in hertz. Zero value means SamplingProfiler is disabled.
+ *
+ * @hide
+ */
+ public static final String SAMPLING_PROFILER_HZ = "sampling_profiler_hz";
+
+ /**
* Settings classname to launch when Settings is clicked from All
* Applications. Needed because of user testing between the old
* and new Settings apps.
@@ -3573,20 +3581,8 @@ public final class Settings {
// If a shortcut is supplied, and it is already defined for
// another bookmark, then remove the old definition.
if (shortcut != 0) {
- Cursor c = cr.query(CONTENT_URI,
- sShortcutProjection, sShortcutSelection,
- new String[] { String.valueOf((int) shortcut) }, null);
- try {
- if (c.moveToFirst()) {
- while (c.getCount() > 0) {
- if (!c.deleteRow()) {
- Log.w(TAG, "Could not delete existing shortcut row");
- }
- }
- }
- } finally {
- if (c != null) c.close();
- }
+ cr.delete(CONTENT_URI, sShortcutSelection,
+ new String[] { String.valueOf((int) shortcut) });
}
ContentValues values = new ContentValues();
diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java
index 35a582d..9b7a73d 100644
--- a/core/java/android/server/BluetoothEventLoop.java
+++ b/core/java/android/server/BluetoothEventLoop.java
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothInputDevice;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.content.Intent;
@@ -427,6 +428,20 @@ class BluetoothEventLoop {
}
}
+ private void onInputDevicePropertyChanged(String path, String[] propValues) {
+ String address = mBluetoothService.getAddressFromObjectPath(path);
+ if (address == null) {
+ Log.e(TAG, "onInputDevicePropertyChanged: Address of the remote device in null");
+ return;
+ }
+ log(" Input Device : Name of Property is:" + propValues[0]);
+ boolean state = false;
+ if (propValues[1].equals("true")) {
+ state = true;
+ }
+ mBluetoothService.handleInputDevicePropertyChange(address, state);
+ }
+
private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) {
String address = mBluetoothService.getAddressFromObjectPath(objectPath);
if (address == null) {
@@ -573,6 +588,8 @@ class BluetoothEventLoop {
}
private boolean onAgentAuthorize(String objectPath, String deviceUuid) {
+ if (!mBluetoothService.isEnabled()) return false;
+
String address = mBluetoothService.getAddressFromObjectPath(objectPath);
if (address == null) {
Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize");
@@ -581,15 +598,15 @@ class BluetoothEventLoop {
boolean authorized = false;
ParcelUuid uuid = ParcelUuid.fromString(deviceUuid);
- BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
// Bluez sends the UUID of the local service being accessed, _not_ the
// remote service
- if (mBluetoothService.isEnabled() &&
- (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid)
- || BluetoothUuid.isAdvAudioDist(uuid)) &&
- !isOtherSinkInNonDisconnectingState(address)) {
- BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if ((BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid)
+ || BluetoothUuid.isAdvAudioDist(uuid)) &&
+ !isOtherSinkInNonDisconnectingState(address)) {
+ BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
+
authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF;
if (authorized) {
Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address);
@@ -597,6 +614,15 @@ class BluetoothEventLoop {
} else {
Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address);
}
+ } else if (BluetoothUuid.isInputDevice(uuid) && !isOtherInputDeviceConnected(address)) {
+ BluetoothInputDevice inputDevice = new BluetoothInputDevice(mContext);
+ authorized = inputDevice.getInputDevicePriority(device) >
+ BluetoothInputDevice.PRIORITY_OFF;
+ if (authorized) {
+ Log.i(TAG, "Allowing incoming HID connection from " + address);
+ } else {
+ Log.i(TAG, "Rejecting incoming HID connection from " + address);
+ }
} else {
Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address);
}
@@ -604,7 +630,19 @@ class BluetoothEventLoop {
return authorized;
}
- boolean isOtherSinkInNonDisconnectingState(String address) {
+ private boolean isOtherInputDeviceConnected(String address) {
+ Set<BluetoothDevice> devices =
+ mBluetoothService.lookupInputDevicesMatchingStates(new int[] {
+ BluetoothInputDevice.STATE_CONNECTING,
+ BluetoothInputDevice.STATE_CONNECTED});
+
+ for (BluetoothDevice device : devices) {
+ if (!device.getAddress().equals(address)) return true;
+ }
+ return false;
+ }
+
+ private boolean isOtherSinkInNonDisconnectingState(String address) {
BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
Set<BluetoothDevice> devices = a2dp.getNonDisconnectedSinks();
if (devices.size() == 0) return false;
diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java
index 31e5a7b..ec99b0d 100644
--- a/core/java/android/server/BluetoothService.java
+++ b/core/java/android/server/BluetoothService.java
@@ -24,16 +24,19 @@
package android.server;
+import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothDeviceProfileState;
import android.bluetooth.BluetoothProfileState;
+import android.bluetooth.BluetoothInputDevice;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetooth;
import android.bluetooth.IBluetoothCallback;
+import android.bluetooth.IBluetoothHeadset;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -70,8 +73,10 @@ import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
+import java.util.Set;
public class BluetoothService extends IBluetooth.Stub {
private static final String TAG = "BluetoothService";
@@ -129,6 +134,8 @@ public class BluetoothService extends IBluetooth.Stub {
private final BluetoothProfileState mHfpProfileState;
private BluetoothA2dpService mA2dpService;
+ private final HashMap<BluetoothDevice, Integer> mInputDevices;
+
private static String mDockAddress;
private String mDockPin;
@@ -198,6 +205,7 @@ public class BluetoothService extends IBluetooth.Stub {
filter.addAction(Intent.ACTION_DOCK_EVENT);
mContext.registerReceiver(mReceiver, filter);
+ mInputDevices = new HashMap<BluetoothDevice, Integer>();
}
public static synchronized String readDockBluetoothAddress() {
@@ -1220,6 +1228,127 @@ public class BluetoothService extends IBluetooth.Stub {
return sp.contains(SHARED_PREFERENCE_DOCK_ADDRESS + address);
}
+ public synchronized boolean connectInputDevice(BluetoothDevice device) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+ "Need BLUETOOTH_ADMIN permission");
+
+ String objectPath = getObjectPathFromAddress(device.getAddress());
+ if (objectPath == null || getConnectedInputDevices().length != 0 ||
+ getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_OFF) {
+ return false;
+ }
+ if(connectInputDeviceNative(objectPath)) {
+ handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTING);
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized boolean disconnectInputDevice(BluetoothDevice device) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+ "Need BLUETOOTH_ADMIN permission");
+
+ String objectPath = getObjectPathFromAddress(device.getAddress());
+ if (objectPath == null || getConnectedInputDevices().length == 0) {
+ return false;
+ }
+ if(disconnectInputDeviceNative(objectPath)) {
+ handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTING);
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized int getInputDeviceState(BluetoothDevice device) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+
+ if (mInputDevices.get(device) == null) {
+ return BluetoothInputDevice.STATE_DISCONNECTED;
+ }
+ return mInputDevices.get(device);
+ }
+
+ public synchronized BluetoothDevice[] getConnectedInputDevices() {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ Set<BluetoothDevice> devices = lookupInputDevicesMatchingStates(
+ new int[] {BluetoothInputDevice.STATE_CONNECTED});
+ return devices.toArray(new BluetoothDevice[devices.size()]);
+ }
+
+ public synchronized int getInputDevicePriority(BluetoothDevice device) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ return Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()),
+ BluetoothInputDevice.PRIORITY_UNDEFINED);
+ }
+
+ public synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+ "Need BLUETOOTH_ADMIN permission");
+ if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) {
+ return false;
+ }
+ return Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()),
+ priority);
+ }
+
+ /*package*/synchronized Set<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) {
+ Set<BluetoothDevice> inputDevices = new HashSet<BluetoothDevice>();
+ if (mInputDevices.isEmpty()) {
+ return inputDevices;
+ }
+ for (BluetoothDevice device: mInputDevices.keySet()) {
+ int inputDeviceState = getInputDeviceState(device);
+ for (int state : states) {
+ if (state == inputDeviceState) {
+ inputDevices.add(device);
+ break;
+ }
+ }
+ }
+ return inputDevices;
+ }
+
+ private synchronized void handleInputDeviceStateChange(BluetoothDevice device, int state) {
+ int prevState;
+ if (mInputDevices.get(device) == null) {
+ prevState = BluetoothInputDevice.STATE_DISCONNECTED;
+ } else {
+ prevState = mInputDevices.get(device);
+ }
+ if (prevState == state) return;
+
+ mInputDevices.put(device, state);
+
+ if (getInputDevicePriority(device) >
+ BluetoothInputDevice.PRIORITY_OFF &&
+ state == BluetoothInputDevice.STATE_CONNECTING ||
+ state == BluetoothInputDevice.STATE_CONNECTED) {
+ // We have connected or attempting to connect.
+ // Bump priority
+ setInputDevicePriority(device, BluetoothInputDevice.PRIORITY_AUTO_CONNECT);
+ }
+
+ Intent intent = new Intent(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothInputDevice.EXTRA_PREVIOUS_INPUT_DEVICE_STATE, prevState);
+ intent.putExtra(BluetoothInputDevice.EXTRA_INPUT_DEVICE_STATE, state);
+ mContext.sendBroadcast(intent, BLUETOOTH_PERM);
+
+ if (DBG) log("InputDevice state : device: " + device + " State:" + prevState + "->" + state);
+
+ }
+
+ /*package*/ void handleInputDevicePropertyChange(String path, boolean connected) {
+ String address = getAddressFromObjectPath(path);
+ if (address == null) return;
+ int state = connected ? BluetoothInputDevice.STATE_CONNECTED :
+ BluetoothInputDevice.STATE_DISCONNECTED;
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ handleInputDeviceStateChange(device, state);
+ }
+
/*package*/ boolean isRemoteDeviceInCache(String address) {
return (mDeviceProperties.get(address) != null);
}
@@ -2103,4 +2232,6 @@ public class BluetoothService extends IBluetooth.Stub {
short channel);
private native boolean removeServiceRecordNative(int handle);
private native boolean setLinkTimeoutNative(String path, int num_slots);
+ private native boolean connectInputDeviceNative(String path);
+ private native boolean disconnectInputDeviceNative(String path);
}
diff --git a/core/java/android/text/AndroidBidi.java b/core/java/android/text/AndroidBidi.java
index e4f934e..eacd40d 100644
--- a/core/java/android/text/AndroidBidi.java
+++ b/core/java/android/text/AndroidBidi.java
@@ -16,6 +16,8 @@
package android.text;
+import android.text.Layout.Directions;
+
/**
* Access the ICU bidi implementation.
* @hide
@@ -44,5 +46,132 @@ package android.text;
return result;
}
+ /**
+ * Returns run direction information for a line within a paragraph.
+ *
+ * @param dir base line direction, either Layout.DIR_LEFT_TO_RIGHT or
+ * Layout.DIR_RIGHT_TO_LEFT
+ * @param levels levels as returned from {@link #bidi}
+ * @param lstart start of the line in the levels array
+ * @param chars the character array (used to determine whitespace)
+ * @param cstart the start of the line in the chars array
+ * @param len the length of the line
+ * @return the directions
+ */
+ public static Directions directions(int dir, byte[] levels, int lstart,
+ char[] chars, int cstart, int len) {
+
+ int baseLevel = dir == Layout.DIR_LEFT_TO_RIGHT ? 0 : 1;
+ int curLevel = levels[lstart];
+ int minLevel = curLevel;
+ int runCount = 1;
+ for (int i = lstart + 1, e = lstart + len; i < e; ++i) {
+ int level = levels[i];
+ if (level != curLevel) {
+ curLevel = level;
+ ++runCount;
+ }
+ }
+
+ // add final run for trailing counter-directional whitespace
+ int visLen = len;
+ if ((curLevel & 1) != (baseLevel & 1)) {
+ // look for visible end
+ while (--visLen >= 0) {
+ char ch = chars[cstart + visLen];
+
+ if (ch == '\n') {
+ --visLen;
+ break;
+ }
+
+ if (ch != ' ' && ch != '\t') {
+ break;
+ }
+ }
+ ++visLen;
+ if (visLen != len) {
+ ++runCount;
+ }
+ }
+
+ if (runCount == 1 && minLevel == baseLevel) {
+ // we're done, only one run on this line
+ if ((minLevel & 1) != 0) {
+ return Layout.DIRS_ALL_RIGHT_TO_LEFT;
+ }
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ }
+
+ int[] ld = new int[runCount * 2];
+ int maxLevel = minLevel;
+ int levelBits = minLevel << Layout.RUN_LEVEL_SHIFT;
+ {
+ // Start of first pair is always 0, we write
+ // length then start at each new run, and the
+ // last run length after we're done.
+ int n = 1;
+ int prev = lstart;
+ curLevel = minLevel;
+ for (int i = lstart, e = lstart + visLen; i < e; ++i) {
+ int level = levels[i];
+ if (level != curLevel) {
+ curLevel = level;
+ if (level > maxLevel) {
+ maxLevel = level;
+ } else if (level < minLevel) {
+ minLevel = level;
+ }
+ // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT
+ ld[n++] = (i - prev) | levelBits;
+ ld[n++] = i - lstart;
+ levelBits = curLevel << Layout.RUN_LEVEL_SHIFT;
+ prev = i;
+ }
+ }
+ ld[n] = (lstart + visLen - prev) | levelBits;
+ if (visLen < len) {
+ ld[++n] = visLen;
+ ld[++n] = (len - visLen) | (baseLevel << Layout.RUN_LEVEL_SHIFT);
+ }
+ }
+
+ // See if we need to swap any runs.
+ // If the min level run direction doesn't match the base
+ // direction, we always need to swap (at this point
+ // we have more than one run).
+ // Otherwise, we don't need to swap the lowest level.
+ // Since there are no logically adjacent runs at the same
+ // level, if the max level is the same as the (new) min
+ // level, we have a series of alternating levels that
+ // is already in order, so there's no more to do.
+ //
+ boolean swap;
+ if ((minLevel & 1) == baseLevel) {
+ minLevel += 1;
+ swap = maxLevel > minLevel;
+ } else {
+ swap = runCount > 1;
+ }
+ if (swap) {
+ for (int level = maxLevel - 1; level >= minLevel; --level) {
+ for (int i = 0; i < ld.length; i += 2) {
+ if (levels[ld[i]] >= level) {
+ int e = i + 2;
+ while (e < ld.length && levels[ld[e]] >= level) {
+ e += 2;
+ }
+ for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) {
+ int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x;
+ x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x;
+ }
+ i = e + 2;
+ }
+ }
+ }
+ }
+ return new Directions(ld);
+ }
+
private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo);
} \ No newline at end of file
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index 944f735..9309b05 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -208,11 +208,11 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback
* width because the width that was passed in was for the
* full text, not the ellipsized form.
*/
- synchronized (sTemp) {
- mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
- source, 0, source.length(),
- null)));
- }
+ TextLine line = TextLine.obtain();
+ line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
+ Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
+ mMax = (int) FloatMath.ceil(line.metrics(null));
+ TextLine.recycle(line);
}
if (includepad) {
@@ -276,14 +276,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback
if (fm == null) {
fm = new Metrics();
}
-
- int wid;
- synchronized (sTemp) {
- wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
- text, 0, text.length(), fm)));
- }
- fm.width = wid;
+ TextLine line = TextLine.obtain();
+ line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT,
+ Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
+ fm.width = (int) FloatMath.ceil(line.metrics(fm));
+ TextLine.recycle(line);
+
return fm;
} else {
return null;
@@ -389,7 +388,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback
public static class Metrics extends Paint.FontMetricsInt {
public int width;
-
+
@Override public String toString() {
return super.toString() + " width=" + width;
}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 14e5655..b6aa03a 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -310,7 +310,6 @@ extends Layout
Directions[] objects = new Directions[1];
-
for (int i = 0; i < n; i++) {
ints[START] = reflowed.getLineStart(i) |
(reflowed.getParagraphDirection(i) << DIR_SHIFT) |
diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java
index c3bd0ae..d426d12 100644
--- a/core/java/android/text/GraphicsOperations.java
+++ b/core/java/android/text/GraphicsOperations.java
@@ -34,13 +34,33 @@ extends CharSequence
float x, float y, Paint p);
/**
+ * Just like {@link Canvas#drawTextRun}.
+ * {@hide}
+ */
+ void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd,
+ float x, float y, int flags, Paint p);
+
+ /**
* Just like {@link Paint#measureText}.
*/
float measureText(int start, int end, Paint p);
-
/**
* Just like {@link Paint#getTextWidths}.
*/
public int getTextWidths(int start, int end, float[] widths, Paint p);
+
+ /**
+ * Just like {@link Paint#getTextRunAdvances}.
+ * @hide
+ */
+ float getTextRunAdvances(int start, int end, int contextStart, int contextEnd,
+ int flags, float[] advances, int advancesIndex, Paint paint);
+
+ /**
+ * Just like {@link Paint#getTextRunCursor}.
+ * @hide
+ */
+ int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset,
+ int cursorOpt, Paint p);
}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 38ac9b7..f533944 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -16,29 +16,33 @@
package android.text;
+import com.android.internal.util.ArrayUtils;
+
import android.emoji.EmojiFactory;
-import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
import android.graphics.Path;
-import com.android.internal.util.ArrayUtils;
-
-import junit.framework.Assert;
-import android.text.style.*;
+import android.graphics.Rect;
import android.text.method.TextKeyListener;
+import android.text.style.AlignmentSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineBackgroundSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.ReplacementSpan;
+import android.text.style.TabStopSpan;
+import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
import android.view.KeyEvent;
+import java.util.Arrays;
+
/**
- * A base class that manages text layout in visual elements on
- * the screen.
- * <p>For text that will be edited, use a {@link DynamicLayout},
- * which will be updated as the text changes.
+ * A base class that manages text layout in visual elements on
+ * the screen.
+ * <p>For text that will be edited, use a {@link DynamicLayout},
+ * which will be updated as the text changes.
* For text that will not change, use a {@link StaticLayout}.
*/
public abstract class Layout {
- private static final boolean DEBUG = false;
private static final ParagraphStyle[] NO_PARA_SPANS =
ArrayUtils.emptyArray(ParagraphStyle.class);
@@ -54,9 +58,7 @@ public abstract class Layout {
MIN_EMOJI = -1;
MAX_EMOJI = -1;
}
- };
-
- private RectF mEmojiRect;
+ }
/**
* Return how wide a layout must be in order to display the
@@ -66,7 +68,7 @@ public abstract class Layout {
TextPaint paint) {
return getDesiredWidth(source, 0, source.length(), paint);
}
-
+
/**
* Return how wide a layout must be in order to display the
* specified text slice with one line per paragraph.
@@ -85,8 +87,7 @@ public abstract class Layout {
next = end;
// note, omits trailing paragraph char
- float w = measureText(paint, workPaint,
- source, i, next, null, true, null);
+ float w = measurePara(paint, workPaint, source, i, next);
if (w > need)
need = w;
@@ -116,6 +117,15 @@ public abstract class Layout {
if (width < 0)
throw new IllegalArgumentException("Layout: " + width + " < 0");
+ // Ensure paint doesn't have baselineShift set.
+ // While normally we don't modify the paint the user passed in,
+ // we were already doing this in Styled.drawUniformRun with both
+ // baselineShift and bgColor. We probably should reevaluate bgColor.
+ if (paint != null) {
+ paint.bgColor = 0;
+ paint.baselineShift = 0;
+ }
+
mText = text;
mPaint = paint;
mWorkPaint = new TextPaint();
@@ -175,7 +185,6 @@ public abstract class Layout {
dbottom = sTempRect.bottom;
}
-
int top = 0;
int bottom = getLineTop(getLineCount());
@@ -185,26 +194,28 @@ public abstract class Layout {
if (dbottom < bottom) {
bottom = dbottom;
}
-
- int first = getLineForVertical(top);
+
+ int first = getLineForVertical(top);
int last = getLineForVertical(bottom);
-
+
int previousLineBottom = getLineTop(first);
int previousLineEnd = getLineStart(first);
-
+
TextPaint paint = mPaint;
CharSequence buf = mText;
int width = mWidth;
boolean spannedText = mSpannedText;
ParagraphStyle[] spans = NO_PARA_SPANS;
- int spanend = 0;
+ int spanEnd = 0;
int textLength = 0;
// First, draw LineBackgroundSpans.
- // LineBackgroundSpans know nothing about the alignment or direction of
- // the layout or line. XXX: Should they?
+ // LineBackgroundSpans know nothing about the alignment, margins, or
+ // direction of the layout or line. XXX: Should they?
+ // They are evaluated at each line.
if (spannedText) {
+ Spanned sp = (Spanned) buf;
textLength = buf.length();
for (int i = first; i <= last; i++) {
int start = previousLineEnd;
@@ -216,12 +227,14 @@ public abstract class Layout {
previousLineBottom = lbottom;
int lbaseline = lbottom - getLineDescent(i);
- if (start >= spanend) {
- Spanned sp = (Spanned) buf;
- spanend = sp.nextSpanTransition(start, textLength,
- LineBackgroundSpan.class);
- spans = sp.getSpans(start, spanend,
- LineBackgroundSpan.class);
+ if (start >= spanEnd) {
+ // These should be infrequent, so we'll use this so that
+ // we don't have to check as often.
+ spanEnd = sp.nextSpanTransition(start, textLength,
+ LineBackgroundSpan.class);
+ // All LineBackgroundSpans on a line contribute to its
+ // background.
+ spans = sp.getSpans(start, end, LineBackgroundSpan.class);
}
for (int n = 0; n < spans.length; n++) {
@@ -234,11 +247,11 @@ public abstract class Layout {
}
}
// reset to their original values
- spanend = 0;
+ spanEnd = 0;
previousLineBottom = getLineTop(first);
previousLineEnd = getLineStart(first);
spans = NO_PARA_SPANS;
- }
+ }
// There can be a highlight even without spans if we are drawing
// a non-spanned transformation of a spanned editing buffer.
@@ -255,7 +268,11 @@ public abstract class Layout {
}
Alignment align = mAlignment;
-
+ TabStops tabStops = null;
+ boolean tabStopsIsInitialized = false;
+
+ TextLine tl = TextLine.obtain();
+
// Next draw the lines, one at a time.
// the baseline is the top of the following line minus the current
// line's descent.
@@ -270,19 +287,30 @@ public abstract class Layout {
previousLineBottom = lbottom;
int lbaseline = lbottom - getLineDescent(i);
- boolean isFirstParaLine = false;
- if (spannedText) {
- if (start == 0 || buf.charAt(start - 1) == '\n') {
- isFirstParaLine = true;
- }
- // New batch of paragraph styles, compute the alignment.
- // Last alignment style wins.
- if (start >= spanend) {
- Spanned sp = (Spanned) buf;
- spanend = sp.nextSpanTransition(start, textLength,
+ int dir = getParagraphDirection(i);
+ int left = 0;
+ int right = mWidth;
+
+ if (spannedText) {
+ Spanned sp = (Spanned) buf;
+ boolean isFirstParaLine = (start == 0 ||
+ buf.charAt(start - 1) == '\n');
+
+ // New batch of paragraph styles, collect into spans array.
+ // Compute the alignment, last alignment style wins.
+ // Reset tabStops, we'll rebuild if we encounter a line with
+ // tabs.
+ // We expect paragraph spans to be relatively infrequent, use
+ // spanEnd so that we can check less frequently. Since
+ // paragraph styles ought to apply to entire paragraphs, we can
+ // just collect the ones present at the start of the paragraph.
+ // If spanEnd is before the end of the paragraph, that's not
+ // our problem.
+ if (start >= spanEnd && (i == first || isFirstParaLine)) {
+ spanEnd = sp.nextSpanTransition(start, textLength,
ParagraphStyle.class);
- spans = sp.getSpans(start, spanend, ParagraphStyle.class);
-
+ spans = sp.getSpans(start, spanEnd, ParagraphStyle.class);
+
align = mAlignment;
for (int n = spans.length-1; n >= 0; n--) {
if (spans[n] instanceof AlignmentSpan) {
@@ -290,45 +318,49 @@ public abstract class Layout {
break;
}
}
+
+ tabStopsIsInitialized = false;
}
- }
-
- int dir = getParagraphDirection(i);
- int left = 0;
- int right = mWidth;
- // Draw all leading margin spans. Adjust left or right according
- // to the paragraph direction of the line.
- if (spannedText) {
+ // Draw all leading margin spans. Adjust left or right according
+ // to the paragraph direction of the line.
final int length = spans.length;
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan) {
LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
+ boolean useFirstLineMargin = isFirstParaLine;
+ if (margin instanceof LeadingMarginSpan2) {
+ int count = ((LeadingMarginSpan2) margin).getLeadingMarginLineCount();
+ int startLine = getLineForOffset(sp.getSpanStart(margin));
+ useFirstLineMargin = i < startLine + count;
+ }
if (dir == DIR_RIGHT_TO_LEFT) {
margin.drawLeadingMargin(c, paint, right, dir, ltop,
lbaseline, lbottom, buf,
start, end, isFirstParaLine, this);
-
- right -= margin.getLeadingMargin(isFirstParaLine);
+ right -= margin.getLeadingMargin(useFirstLineMargin);
} else {
margin.drawLeadingMargin(c, paint, left, dir, ltop,
lbaseline, lbottom, buf,
start, end, isFirstParaLine, this);
-
- boolean useMargin = isFirstParaLine;
- if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) {
- int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount();
- useMargin = count > i;
- }
- left += margin.getLeadingMargin(useMargin);
+ left += margin.getLeadingMargin(useFirstLineMargin);
}
}
}
}
- // Adjust the point at which to start rendering depending on the
- // alignment of the paragraph.
+ boolean hasTabOrEmoji = getLineContainsTab(i);
+ // Can't tell if we have tabs for sure, currently
+ if (hasTabOrEmoji && !tabStopsIsInitialized) {
+ if (tabStops == null) {
+ tabStops = new TabStops(TAB_INCREMENT, spans);
+ } else {
+ tabStops.reset(TAB_INCREMENT, spans);
+ }
+ tabStopsIsInitialized = true;
+ }
+
int x;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
@@ -337,41 +369,80 @@ public abstract class Layout {
x = right;
}
} else {
- int max = (int)getLineMax(i, spans, false);
+ int max = (int)getLineExtent(i, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
- if (dir == DIR_RIGHT_TO_LEFT) {
- x = left + max;
- } else {
+ if (dir == DIR_LEFT_TO_RIGHT) {
x = right - max;
- }
- } else {
- // Alignment.ALIGN_CENTER
- max = max & ~1;
- int half = (right - left - max) >> 1;
- if (dir == DIR_RIGHT_TO_LEFT) {
- x = right - half;
} else {
- x = left + half;
+ x = left - max;
}
+ } else { // Alignment.ALIGN_CENTER
+ max = max & ~1;
+ x = (right + left - max) >> 1;
}
}
Directions directions = getLineDirections(i);
- boolean hasTab = getLineContainsTab(i);
if (directions == DIRS_ALL_LEFT_TO_RIGHT &&
- !spannedText && !hasTab) {
- if (DEBUG) {
- Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT);
- Assert.assertNotNull(c);
- }
+ !spannedText && !hasTabOrEmoji) {
// XXX: assumes there's nothing additional to be done
c.drawText(buf, start, end, x, lbaseline, paint);
} else {
- drawText(c, buf, start, end, dir, directions,
- x, ltop, lbaseline, lbottom, paint, mWorkPaint,
- hasTab, spans);
+ tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
+ tl.draw(c, x, ltop, lbaseline, lbottom);
+ }
+ }
+
+ TextLine.recycle(tl);
+ }
+
+ /**
+ * Return the start position of the line, given the left and right bounds
+ * of the margins.
+ *
+ * @param line the line index
+ * @param left the left bounds (0, or leading margin if ltr para)
+ * @param right the right bounds (width, minus leading margin if rtl para)
+ * @return the start position of the line (to right of line if rtl para)
+ */
+ private int getLineStartPos(int line, int left, int right) {
+ // Adjust the point at which to start rendering depending on the
+ // alignment of the paragraph.
+ Alignment align = getParagraphAlignment(line);
+ int dir = getParagraphDirection(line);
+
+ int x;
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ x = left;
+ } else {
+ x = right;
+ }
+ } else {
+ TabStops tabStops = null;
+ if (mSpannedText && getLineContainsTab(line)) {
+ Spanned spanned = (Spanned) mText;
+ int start = getLineStart(line);
+ int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
+ TabStopSpan.class);
+ TabStopSpan[] tabSpans = spanned.getSpans(start, spanEnd, TabStopSpan.class);
+ if (tabSpans.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, tabSpans);
+ }
+ }
+ int max = (int)getLineExtent(line, tabStops, false);
+ if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ x = right - max;
+ } else {
+ x = left - max;
+ }
+ } else { // Alignment.ALIGN_CENTER
+ max = max & ~1;
+ x = (left + right - max) >> 1;
}
}
+ return x;
}
/**
@@ -417,7 +488,7 @@ public abstract class Layout {
mWidth = wid;
}
-
+
/**
* Return the total height of this layout.
*/
@@ -450,7 +521,7 @@ public abstract class Layout {
* Return the number of lines of text in this layout.
*/
public abstract int getLineCount();
-
+
/**
* Return the baseline for the specified line (0&hellip;getLineCount() - 1)
* If bounds is not null, return the top, left, right, bottom extents
@@ -524,13 +595,95 @@ public abstract class Layout {
*/
public abstract int getBottomPadding();
+
+ /**
+ * Returns true if the character at offset and the preceding character
+ * are at different run levels (and thus there's a split caret).
+ * @param offset the offset
+ * @return true if at a level boundary
+ */
+ private boolean isLevelBoundary(int offset) {
+ int line = getLineForOffset(offset);
+ Directions dirs = getLineDirections(line);
+ if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
+ return false;
+ }
+
+ int[] runs = dirs.mDirections;
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ if (offset == lineStart || offset == lineEnd) {
+ int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
+ int runIndex = offset == lineStart ? 0 : runs.length - 2;
+ return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
+ }
+
+ offset -= lineStart;
+ for (int i = 0; i < runs.length; i += 2) {
+ if (offset == runs[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean primaryIsTrailingPrevious(int offset) {
+ int line = getLineForOffset(offset);
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int[] runs = getLineDirections(line).mDirections;
+
+ int levelAt = -1;
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (limit > lineEnd) {
+ limit = lineEnd;
+ }
+ if (offset >= start && offset < limit) {
+ if (offset > start) {
+ // Previous character is at same level, so don't use trailing.
+ return false;
+ }
+ levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+ break;
+ }
+ }
+ if (levelAt == -1) {
+ // Offset was limit of line.
+ levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
+ }
+
+ // At level boundary, check previous level.
+ int levelBefore = -1;
+ if (offset == lineStart) {
+ levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
+ } else {
+ offset -= 1;
+ for (int i = 0; i < runs.length; i += 2) {
+ int start = lineStart + runs[i];
+ int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+ if (limit > lineEnd) {
+ limit = lineEnd;
+ }
+ if (offset >= start && offset < limit) {
+ levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+ break;
+ }
+ }
+ }
+
+ return levelBefore < levelAt;
+ }
+
/**
* Get the primary horizontal position for the specified text offset.
* This is the location where a new character would be inserted in
* the paragraph's primary direction.
*/
public float getPrimaryHorizontal(int offset) {
- return getHorizontal(offset, false, true);
+ boolean trailing = primaryIsTrailingPrevious(offset);
+ return getHorizontal(offset, trailing);
}
/**
@@ -539,66 +692,42 @@ public abstract class Layout {
* the direction other than the paragraph's primary direction.
*/
public float getSecondaryHorizontal(int offset) {
- return getHorizontal(offset, true, true);
+ boolean trailing = primaryIsTrailingPrevious(offset);
+ return getHorizontal(offset, !trailing);
}
- private float getHorizontal(int offset, boolean trailing, boolean alt) {
+ private float getHorizontal(int offset, boolean trailing) {
int line = getLineForOffset(offset);
- return getHorizontal(offset, trailing, alt, line);
+ return getHorizontal(offset, trailing, line);
}
- private float getHorizontal(int offset, boolean trailing, boolean alt,
- int line) {
+ private float getHorizontal(int offset, boolean trailing, int line) {
int start = getLineStart(line);
- int end = getLineVisibleEnd(line);
+ int end = getLineEnd(line);
int dir = getParagraphDirection(line);
- boolean tab = getLineContainsTab(line);
+ boolean hasTabOrEmoji = getLineContainsTab(line);
Directions directions = getLineDirections(line);
- TabStopSpan[] tabs = null;
- if (tab && mText instanceof Spanned) {
- tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ TabStops tabStops = null;
+ if (hasTabOrEmoji && mText instanceof Spanned) {
+ // Just checking this line should be good enough, tabs should be
+ // consistent across all lines in a paragraph.
+ TabStopSpan[] tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ if (tabs.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
+ }
}
- float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end,
- dir, directions, trailing, alt, tab, tabs);
+ TextLine tl = TextLine.obtain();
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops);
+ float wid = tl.measure(offset - start, trailing, null);
+ TextLine.recycle(tl);
- if (offset > end) {
- if (dir == DIR_RIGHT_TO_LEFT)
- wid -= measureText(mPaint, mWorkPaint,
- mText, end, offset, null, tab, tabs);
- else
- wid += measureText(mPaint, mWorkPaint,
- mText, end, offset, null, tab, tabs);
- }
-
- Alignment align = getParagraphAlignment(line);
int left = getParagraphLeft(line);
int right = getParagraphRight(line);
- if (align == Alignment.ALIGN_NORMAL) {
- if (dir == DIR_RIGHT_TO_LEFT)
- return right + wid;
- else
- return left + wid;
- }
-
- float max = getLineMax(line);
-
- if (align == Alignment.ALIGN_OPPOSITE) {
- if (dir == DIR_RIGHT_TO_LEFT)
- return left + max + wid;
- else
- return right - max + wid;
- } else { /* align == Alignment.ALIGN_CENTER */
- int imax = ((int) max) & ~1;
-
- if (dir == DIR_RIGHT_TO_LEFT)
- return right - (((right - left) - imax) / 2) + wid;
- else
- return left + ((right - left) - imax) / 2 + wid;
- }
+ return getLineStartPos(line, left, right) + wid;
}
/**
@@ -656,38 +785,76 @@ public abstract class Layout {
}
/**
- * Gets the horizontal extent of the specified line, excluding
- * trailing whitespace.
+ * Gets the unsigned horizontal extent of the specified line, including
+ * leading margin indent, but excluding trailing whitespace.
*/
public float getLineMax(int line) {
- return getLineMax(line, null, false);
+ float margin = getParagraphLeadingMargin(line);
+ float signedExtent = getLineExtent(line, false);
+ return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
}
/**
- * Gets the horizontal extent of the specified line, including
- * trailing whitespace.
+ * Gets the unsigned horizontal extent of the specified line, including
+ * leading margin indent and trailing whitespace.
*/
public float getLineWidth(int line) {
- return getLineMax(line, null, true);
+ float margin = getParagraphLeadingMargin(line);
+ float signedExtent = getLineExtent(line, true);
+ return margin + signedExtent >= 0 ? signedExtent : -signedExtent;
}
- private float getLineMax(int line, Object[] tabs, boolean full) {
+ /**
+ * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
+ * tab stops instead of using the ones passed in.
+ * @param line the index of the line
+ * @param full whether to include trailing whitespace
+ * @return the extent of the line
+ */
+ private float getLineExtent(int line, boolean full) {
int start = getLineStart(line);
- int end;
+ int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
+
+ boolean hasTabsOrEmoji = getLineContainsTab(line);
+ TabStops tabStops = null;
+ if (hasTabsOrEmoji && mText instanceof Spanned) {
+ // Just checking this line should be good enough, tabs should be
+ // consistent across all lines in a paragraph.
+ TabStopSpan[] tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ if (tabs.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
+ }
+ }
+ Directions directions = getLineDirections(line);
+ int dir = getParagraphDirection(line);
- if (full) {
- end = getLineEnd(line);
- } else {
- end = getLineVisibleEnd(line);
- }
- boolean tab = getLineContainsTab(line);
+ TextLine tl = TextLine.obtain();
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
+ float width = tl.metrics(null);
+ TextLine.recycle(tl);
+ return width;
+ }
- if (tabs == null && tab && mText instanceof Spanned) {
- tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
- }
+ /**
+ * Returns the signed horizontal extent of the specified line, excluding
+ * leading margin. If full is false, excludes trailing whitespace.
+ * @param line the index of the line
+ * @param tabStops the tab stops, can be null if we know they're not used.
+ * @param full whether to include trailing whitespace
+ * @return the extent of the text on this line
+ */
+ private float getLineExtent(int line, TabStops tabStops, boolean full) {
+ int start = getLineStart(line);
+ int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
+ boolean hasTabsOrEmoji = getLineContainsTab(line);
+ Directions directions = getLineDirections(line);
+ int dir = getParagraphDirection(line);
- return measureText(mPaint, mWorkPaint,
- mText, start, end, null, tab, tabs);
+ TextLine tl = TextLine.obtain();
+ tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
+ float width = tl.metrics(null);
+ TextLine.recycle(tl);
+ return width;
}
/**
@@ -738,7 +905,7 @@ public abstract class Layout {
}
/**
- * Get the character offset on the specfied line whose position is
+ * Get the character offset on the specified line whose position is
* closest to the specified horizontal position.
*/
public int getOffsetForHorizontal(int line, float horiz) {
@@ -752,14 +919,13 @@ public abstract class Layout {
int best = min;
float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
- int here = min;
- for (int i = 0; i < dirs.mDirections.length; i++) {
- int there = here + dirs.mDirections[i];
- int swap = ((i & 1) == 0) ? 1 : -1;
+ for (int i = 0; i < dirs.mDirections.length; i += 2) {
+ int here = min + dirs.mDirections[i];
+ int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
+ int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
if (there > max)
there = max;
-
int high = there - 1 + 1, low = here + 1 - 1, guess;
while (high - low > 1) {
@@ -792,7 +958,7 @@ public abstract class Layout {
if (dist < bestdist) {
bestdist = dist;
- best = low;
+ best = low;
}
}
@@ -802,8 +968,6 @@ public abstract class Layout {
bestdist = dist;
best = here;
}
-
- here = there;
}
float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
@@ -823,19 +987,15 @@ public abstract class Layout {
return getLineStart(line + 1);
}
- /**
+ /**
* Return the text offset after the last visible character (so whitespace
* is not counted) on the specified line.
*/
public int getLineVisibleEnd(int line) {
return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
}
-
- private int getLineVisibleEnd(int line, int start, int end) {
- if (DEBUG) {
- Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end);
- }
+ private int getLineVisibleEnd(int line, int start, int end) {
CharSequence text = mText;
char ch;
if (line == getLineCount() - 1) {
@@ -882,207 +1042,62 @@ public abstract class Layout {
return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
}
- /**
- * Return the text offset that would be reached by moving left
- * (possibly onto another line) from the specified offset.
- */
public int getOffsetToLeftOf(int offset) {
- int line = getLineForOffset(offset);
- int start = getLineStart(line);
- int end = getLineEnd(line);
- Directions dirs = getLineDirections(line);
-
- if (line != getLineCount() - 1)
- end--;
-
- float horiz = getPrimaryHorizontal(offset);
-
- int best = offset;
- float besth = Integer.MIN_VALUE;
- int candidate;
-
- candidate = TextUtils.getOffsetBefore(mText, offset);
- if (candidate >= start && candidate <= end) {
- float h = getPrimaryHorizontal(candidate);
-
- if (h < horiz && h > besth) {
- best = candidate;
- besth = h;
- }
- }
-
- candidate = TextUtils.getOffsetAfter(mText, offset);
- if (candidate >= start && candidate <= end) {
- float h = getPrimaryHorizontal(candidate);
-
- if (h < horiz && h > besth) {
- best = candidate;
- besth = h;
- }
- }
-
- int here = start;
- for (int i = 0; i < dirs.mDirections.length; i++) {
- int there = here + dirs.mDirections[i];
- if (there > end)
- there = end;
-
- float h = getPrimaryHorizontal(here);
-
- if (h < horiz && h > besth) {
- best = here;
- besth = h;
- }
-
- candidate = TextUtils.getOffsetAfter(mText, here);
- if (candidate >= start && candidate <= end) {
- h = getPrimaryHorizontal(candidate);
-
- if (h < horiz && h > besth) {
- best = candidate;
- besth = h;
- }
- }
-
- candidate = TextUtils.getOffsetBefore(mText, there);
- if (candidate >= start && candidate <= end) {
- h = getPrimaryHorizontal(candidate);
-
- if (h < horiz && h > besth) {
- best = candidate;
- besth = h;
- }
- }
-
- here = there;
- }
-
- float h = getPrimaryHorizontal(end);
-
- if (h < horiz && h > besth) {
- best = end;
- besth = h;
- }
-
- if (best != offset)
- return best;
-
- int dir = getParagraphDirection(line);
-
- if (dir > 0) {
- if (line == 0)
- return best;
- else
- return getOffsetForHorizontal(line - 1, 10000);
- } else {
- if (line == getLineCount() - 1)
- return best;
- else
- return getOffsetForHorizontal(line + 1, 10000);
- }
+ return getOffsetToLeftRightOf(offset, true);
}
- /**
- * Return the text offset that would be reached by moving right
- * (possibly onto another line) from the specified offset.
- */
public int getOffsetToRightOf(int offset) {
- int line = getLineForOffset(offset);
- int start = getLineStart(line);
- int end = getLineEnd(line);
- Directions dirs = getLineDirections(line);
-
- if (line != getLineCount() - 1)
- end--;
-
- float horiz = getPrimaryHorizontal(offset);
-
- int best = offset;
- float besth = Integer.MAX_VALUE;
- int candidate;
-
- candidate = TextUtils.getOffsetBefore(mText, offset);
- if (candidate >= start && candidate <= end) {
- float h = getPrimaryHorizontal(candidate);
-
- if (h > horiz && h < besth) {
- best = candidate;
- besth = h;
- }
- }
-
- candidate = TextUtils.getOffsetAfter(mText, offset);
- if (candidate >= start && candidate <= end) {
- float h = getPrimaryHorizontal(candidate);
-
- if (h > horiz && h < besth) {
- best = candidate;
- besth = h;
- }
- }
-
- int here = start;
- for (int i = 0; i < dirs.mDirections.length; i++) {
- int there = here + dirs.mDirections[i];
- if (there > end)
- there = end;
-
- float h = getPrimaryHorizontal(here);
-
- if (h > horiz && h < besth) {
- best = here;
- besth = h;
- }
-
- candidate = TextUtils.getOffsetAfter(mText, here);
- if (candidate >= start && candidate <= end) {
- h = getPrimaryHorizontal(candidate);
+ return getOffsetToLeftRightOf(offset, false);
+ }
- if (h > horiz && h < besth) {
- best = candidate;
- besth = h;
+ private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
+ int line = getLineForOffset(caret);
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int lineDir = getParagraphDirection(line);
+
+ boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
+ if (caret == (advance ? lineEnd : lineStart)) {
+ // walking off line, so look at the line we're headed to
+ if (caret == lineStart) {
+ if (line > 0) {
+ --line;
+ } else {
+ return caret; // at very start, don't move
}
- }
-
- candidate = TextUtils.getOffsetBefore(mText, there);
- if (candidate >= start && candidate <= end) {
- h = getPrimaryHorizontal(candidate);
-
- if (h > horiz && h < besth) {
- best = candidate;
- besth = h;
+ } else {
+ if (line < getLineCount() - 1) {
+ ++line;
+ } else {
+ return caret; // at very end, don't move
}
}
- here = there;
- }
-
- float h = getPrimaryHorizontal(end);
-
- if (h > horiz && h < besth) {
- best = end;
- besth = h;
+ lineStart = getLineStart(line);
+ lineEnd = getLineEnd(line);
+ int newDir = getParagraphDirection(line);
+ if (newDir != lineDir) {
+ // unusual case. we want to walk onto the line, but it runs
+ // in a different direction than this one, so we fake movement
+ // in the opposite direction.
+ toLeft = !toLeft;
+ lineDir = newDir;
+ }
}
- if (best != offset)
- return best;
-
- int dir = getParagraphDirection(line);
+ Directions directions = getLineDirections(line);
- if (dir > 0) {
- if (line == getLineCount() - 1)
- return best;
- else
- return getOffsetForHorizontal(line + 1, -10000);
- } else {
- if (line == 0)
- return best;
- else
- return getOffsetForHorizontal(line - 1, -10000);
- }
+ TextLine tl = TextLine.obtain();
+ // XXX: we don't care about tabs
+ tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
+ caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
+ tl = TextLine.recycle(tl);
+ return caret;
}
private int getOffsetAtStartOf(int offset) {
+ // XXX this probably should skip local reorderings and
+ // zero-width characters, look at callers
if (offset == 0)
return 0;
@@ -1115,7 +1130,7 @@ public abstract class Layout {
/**
* Fills in the specified Path with a representation of a cursor
* at the specified offset. This will often be a vertical line
- * but can be multiple discontinous lines in text with multiple
+ * but can be multiple discontinuous lines in text with multiple
* directionalities.
*/
public void getCursorPath(int point, Path dest,
@@ -1127,7 +1142,8 @@ public abstract class Layout {
int bottom = getLineTop(line+1);
float h1 = getPrimaryHorizontal(point) - 0.5f;
- float h2 = getSecondaryHorizontal(point) - 0.5f;
+ float h2 = isLevelBoundary(point) ?
+ getSecondaryHorizontal(point) - 0.5f : h1;
int caps = TextKeyListener.getMetaState(editingBuffer,
KeyEvent.META_SHIFT_ON) |
@@ -1204,9 +1220,10 @@ public abstract class Layout {
if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
lineend--;
- int here = linestart;
- for (int i = 0; i < dirs.mDirections.length; i++) {
- int there = here + dirs.mDirections[i];
+ for (int i = 0; i < dirs.mDirections.length; i += 2) {
+ int here = linestart + dirs.mDirections[i];
+ int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
+
if (there > lineend)
there = lineend;
@@ -1215,14 +1232,12 @@ public abstract class Layout {
int en = Math.min(end, there);
if (st != en) {
- float h1 = getHorizontal(st, false, false, line);
- float h2 = getHorizontal(en, true, false, line);
+ float h1 = getHorizontal(st, false, line);
+ float h2 = getHorizontal(en, true, line);
dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
}
}
-
- here = there;
}
}
@@ -1257,7 +1272,7 @@ public abstract class Layout {
addSelection(startline, start, getLineEnd(startline),
top, getLineBottom(startline), dest);
-
+
if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
dest.addRect(getLineLeft(startline), top,
0, getLineBottom(startline), Path.Direction.CW);
@@ -1310,422 +1325,173 @@ public abstract class Layout {
* Get the left edge of the specified paragraph, inset by left margins.
*/
public final int getParagraphLeft(int line) {
- int dir = getParagraphDirection(line);
-
int left = 0;
-
- boolean par = false;
- int off = getLineStart(line);
- if (off == 0 || mText.charAt(off - 1) == '\n')
- par = true;
-
- if (dir == DIR_LEFT_TO_RIGHT) {
- if (mSpannedText) {
- Spanned sp = (Spanned) mText;
- LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
- getLineEnd(line),
- LeadingMarginSpan.class);
-
- for (int i = 0; i < spans.length; i++) {
- boolean margin = par;
- LeadingMarginSpan span = spans[i];
- if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) {
- int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount();
- margin = count >= line;
- }
- left += span.getLeadingMargin(margin);
- }
- }
+ int dir = getParagraphDirection(line);
+ if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
+ return left; // leading margin has no impact, or no styles
}
-
- return left;
+ return getParagraphLeadingMargin(line);
}
/**
* Get the right edge of the specified paragraph, inset by right margins.
*/
public final int getParagraphRight(int line) {
- int dir = getParagraphDirection(line);
-
int right = mWidth;
-
- boolean par = false;
- int off = getLineStart(line);
- if (off == 0 || mText.charAt(off - 1) == '\n')
- par = true;
-
-
- if (dir == DIR_RIGHT_TO_LEFT) {
- if (mSpannedText) {
- Spanned sp = (Spanned) mText;
- LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
- getLineEnd(line),
- LeadingMarginSpan.class);
-
- for (int i = 0; i < spans.length; i++) {
- right -= spans[i].getLeadingMargin(par);
- }
- }
+ int dir = getParagraphDirection(line);
+ if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
+ return right; // leading margin has no impact, or no styles
}
-
- return right;
+ return right - getParagraphLeadingMargin(line);
}
- private void drawText(Canvas canvas,
- CharSequence text, int start, int end,
- int dir, Directions directions,
- float x, int top, int y, int bottom,
- TextPaint paint,
- TextPaint workPaint,
- boolean hasTabs, Object[] parspans) {
- char[] buf;
- if (!hasTabs) {
- if (directions == DIRS_ALL_LEFT_TO_RIGHT) {
- if (DEBUG) {
- Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir);
- }
- Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false);
- return;
- }
- buf = null;
- } else {
- buf = TextUtils.obtain(end - start);
- TextUtils.getChars(text, start, end, buf, 0);
- }
-
- float h = 0;
-
- int here = 0;
- for (int i = 0; i < directions.mDirections.length; i++) {
- int there = here + directions.mDirections[i];
- if (there > end - start)
- there = end - start;
-
- int segstart = here;
- for (int j = hasTabs ? here : there; j <= there; j++) {
- if (j == there || buf[j] == '\t') {
- h += Styled.drawText(canvas, text,
- start + segstart, start + j,
- dir, (i & 1) != 0, x + h,
- top, y, bottom, paint, workPaint,
- start + j != end);
-
- if (j != there && buf[j] == '\t')
- h = dir * nextTab(text, start, end, h * dir, parspans);
-
- segstart = j + 1;
- } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) {
- int emoji = Character.codePointAt(buf, j);
-
- if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
- Bitmap bm = EMOJI_FACTORY.
- getBitmapFromAndroidPua(emoji);
-
- if (bm != null) {
- h += Styled.drawText(canvas, text,
- start + segstart, start + j,
- dir, (i & 1) != 0, x + h,
- top, y, bottom, paint, workPaint,
- start + j != end);
-
- if (mEmojiRect == null) {
- mEmojiRect = new RectF();
- }
+ /**
+ * Returns the effective leading margin (unsigned) for this line,
+ * taking into account LeadingMarginSpan and LeadingMarginSpan2.
+ * @param line the line index
+ * @return the leading margin of this line
+ */
+ private int getParagraphLeadingMargin(int line) {
+ if (!mSpannedText) {
+ return 0;
+ }
+ Spanned spanned = (Spanned) mText;
- workPaint.set(paint);
- Styled.measureText(paint, workPaint, text,
- start + j, start + j + 1,
- null);
-
- float bitmapHeight = bm.getHeight();
- float textHeight = -workPaint.ascent();
- float scale = textHeight / bitmapHeight;
- float width = bm.getWidth() * scale;
+ int lineStart = getLineStart(line);
+ int lineEnd = getLineEnd(line);
+ int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
+ LeadingMarginSpan.class);
+ LeadingMarginSpan[] spans = spanned.getSpans(lineStart, spanEnd,
+ LeadingMarginSpan.class);
+ if (spans.length == 0) {
+ return 0; // no leading margin span;
+ }
- mEmojiRect.set(x + h, y - textHeight,
- x + h + width, y);
+ int margin = 0;
- canvas.drawBitmap(bm, null, mEmojiRect, paint);
- h += width;
+ boolean isFirstParaLine = lineStart == 0 ||
+ spanned.charAt(lineStart - 1) == '\n';
- j++;
- segstart = j + 1;
- }
- }
- }
+ for (int i = 0; i < spans.length; i++) {
+ LeadingMarginSpan span = spans[i];
+ boolean useFirstLineMargin = isFirstParaLine;
+ if (span instanceof LeadingMarginSpan2) {
+ int spStart = spanned.getSpanStart(span);
+ int spanLine = getLineForOffset(spStart);
+ int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount();
+ useFirstLineMargin = line < spanLine + count;
}
-
- here = there;
+ margin += span.getLeadingMargin(useFirstLineMargin);
}
- if (hasTabs)
- TextUtils.recycle(buf);
+ return margin;
}
- private static float measureText(TextPaint paint,
- TextPaint workPaint,
- CharSequence text,
- int start, int offset, int end,
- int dir, Directions directions,
- boolean trailing, boolean alt,
- boolean hasTabs, Object[] tabs) {
- char[] buf = null;
-
- if (hasTabs) {
- buf = TextUtils.obtain(end - start);
- TextUtils.getChars(text, start, end, buf, 0);
- }
-
- float h = 0;
-
- if (alt) {
- if (dir == DIR_RIGHT_TO_LEFT)
- trailing = !trailing;
- }
-
- int here = 0;
- for (int i = 0; i < directions.mDirections.length; i++) {
- if (alt)
- trailing = !trailing;
-
- int there = here + directions.mDirections[i];
- if (there > end - start)
- there = end - start;
-
- int segstart = here;
- for (int j = hasTabs ? here : there; j <= there; j++) {
- int codept = 0;
- Bitmap bm = null;
-
- if (hasTabs && j < there) {
- codept = buf[j];
- }
-
- if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) {
- codept = Character.codePointAt(buf, j);
-
- if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
- bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
- }
- }
-
- if (j == there || codept == '\t' || bm != null) {
- float segw;
-
- if (offset < start + j ||
- (trailing && offset <= start + j)) {
- if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) {
- h += Styled.measureText(paint, workPaint, text,
- start + segstart, offset,
- null);
- return h;
- }
-
- if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) {
- h -= Styled.measureText(paint, workPaint, text,
- start + segstart, offset,
- null);
- return h;
- }
- }
-
- segw = Styled.measureText(paint, workPaint, text,
- start + segstart, start + j,
- null);
-
- if (offset < start + j ||
- (trailing && offset <= start + j)) {
- if (dir == DIR_LEFT_TO_RIGHT) {
- h += segw - Styled.measureText(paint, workPaint,
- text,
- start + segstart,
- offset, null);
- return h;
- }
-
- if (dir == DIR_RIGHT_TO_LEFT) {
- h -= segw - Styled.measureText(paint, workPaint,
- text,
- start + segstart,
- offset, null);
- return h;
- }
- }
-
- if (dir == DIR_RIGHT_TO_LEFT)
- h -= segw;
- else
- h += segw;
-
- if (j != there && buf[j] == '\t') {
- if (offset == start + j)
- return h;
-
- h = dir * nextTab(text, start, end, h * dir, tabs);
- }
-
- if (bm != null) {
- workPaint.set(paint);
- Styled.measureText(paint, workPaint, text,
- j, j + 2, null);
-
- float wid = (float) bm.getWidth() *
- -workPaint.ascent() / bm.getHeight();
-
- if (dir == DIR_RIGHT_TO_LEFT) {
- h -= wid;
- } else {
- h += wid;
+ /* package */
+ static float measurePara(TextPaint paint, TextPaint workPaint,
+ CharSequence text, int start, int end) {
+
+ MeasuredText mt = MeasuredText.obtain();
+ TextLine tl = TextLine.obtain();
+ try {
+ mt.setPara(text, start, end, DIR_REQUEST_LTR);
+ Directions directions;
+ int dir;
+ if (mt.mEasy) {
+ directions = DIRS_ALL_LEFT_TO_RIGHT;
+ dir = Layout.DIR_LEFT_TO_RIGHT;
+ } else {
+ directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
+ 0, mt.mChars, 0, mt.mLen);
+ dir = mt.mDir;
+ }
+ char[] chars = mt.mChars;
+ int len = mt.mLen;
+ boolean hasTabs = false;
+ TabStops tabStops = null;
+ for (int i = 0; i < len; ++i) {
+ if (chars[i] == '\t') {
+ hasTabs = true;
+ if (text instanceof Spanned) {
+ Spanned spanned = (Spanned) text;
+ int spanEnd = spanned.nextSpanTransition(start, end,
+ TabStopSpan.class);
+ TabStopSpan[] spans = spanned.getSpans(start, spanEnd,
+ TabStopSpan.class);
+ if (spans.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, spans);
}
-
- j++;
}
-
- segstart = j + 1;
+ break;
}
}
-
- here = there;
+ tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
+ return tl.metrics(null);
+ } finally {
+ TextLine.recycle(tl);
+ MeasuredText.recycle(mt);
}
-
- if (hasTabs)
- TextUtils.recycle(buf);
-
- return h;
}
/**
- * Measure width of a run of text on a single line that is known to all be
- * in the same direction as the paragraph base direction. Returns the width,
- * and the line metrics in fm if fm is not null.
- *
- * @param paint the paint for the text; will not be modified
- * @param workPaint paint available for modification
- * @param text text
- * @param start start of the line
- * @param end limit of the line
- * @param fm object to return integer metrics in, can be null
- * @param hasTabs true if it is known that the line has tabs
- * @param tabs tab position information
- * @return the width of the text from start to end
+ * @hide
*/
- /* package */ static float measureText(TextPaint paint,
- TextPaint workPaint,
- CharSequence text,
- int start, int end,
- Paint.FontMetricsInt fm,
- boolean hasTabs, Object[] tabs) {
- char[] buf = null;
-
- if (hasTabs) {
- buf = TextUtils.obtain(end - start);
- TextUtils.getChars(text, start, end, buf, 0);
- }
-
- int len = end - start;
-
- int lastPos = 0;
- float width = 0;
- int ascent = 0, descent = 0, top = 0, bottom = 0;
-
- if (fm != null) {
- fm.ascent = 0;
- fm.descent = 0;
- }
-
- for (int pos = hasTabs ? 0 : len; pos <= len; pos++) {
- int codept = 0;
- Bitmap bm = null;
-
- if (hasTabs && pos < len) {
- codept = buf[pos];
- }
-
- if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) {
- codept = Character.codePointAt(buf, pos);
-
- if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
- bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
- }
- }
-
- if (pos == len || codept == '\t' || bm != null) {
- workPaint.baselineShift = 0;
-
- width += Styled.measureText(paint, workPaint, text,
- start + lastPos, start + pos,
- fm);
-
- if (fm != null) {
- if (workPaint.baselineShift < 0) {
- fm.ascent += workPaint.baselineShift;
- fm.top += workPaint.baselineShift;
- } else {
- fm.descent += workPaint.baselineShift;
- fm.bottom += workPaint.baselineShift;
+ /* package */ static class TabStops {
+ private int[] mStops;
+ private int mNumStops;
+ private int mIncrement;
+
+ TabStops(int increment, Object[] spans) {
+ reset(increment, spans);
+ }
+
+ void reset(int increment, Object[] spans) {
+ this.mIncrement = increment;
+
+ int ns = 0;
+ if (spans != null) {
+ int[] stops = this.mStops;
+ for (Object o : spans) {
+ if (o instanceof TabStopSpan) {
+ if (stops == null) {
+ stops = new int[10];
+ } else if (ns == stops.length) {
+ int[] nstops = new int[ns * 2];
+ for (int i = 0; i < ns; ++i) {
+ nstops[i] = stops[i];
+ }
+ stops = nstops;
+ }
+ stops[ns++] = ((TabStopSpan) o).getTabStop();
}
}
-
- if (pos != len) {
- if (bm == null) {
- // no emoji, must have hit a tab
- width = nextTab(text, start, end, width, tabs);
- } else {
- // This sets up workPaint with the font on the emoji
- // text, so that we can extract the ascent and scale.
-
- // We can't use the result of the previous call to
- // measureText because the emoji might have its own style.
- // We have to initialize workPaint here because if the
- // text is unstyled measureText might not use workPaint
- // at all.
- workPaint.set(paint);
- Styled.measureText(paint, workPaint, text,
- start + pos, start + pos + 1, null);
-
- width += (float) bm.getWidth() *
- -workPaint.ascent() / bm.getHeight();
-
- // Since we had an emoji, we bump past the second half
- // of the surrogate pair.
- pos++;
- }
+ if (ns > 1) {
+ Arrays.sort(stops, 0, ns);
}
+ if (stops != this.mStops) {
+ this.mStops = stops;
+ }
+ }
+ this.mNumStops = ns;
+ }
- if (fm != null) {
- if (fm.ascent < ascent) {
- ascent = fm.ascent;
- }
- if (fm.descent > descent) {
- descent = fm.descent;
- }
-
- if (fm.top < top) {
- top = fm.top;
- }
- if (fm.bottom > bottom) {
- bottom = fm.bottom;
+ float nextTab(float h) {
+ int ns = this.mNumStops;
+ if (ns > 0) {
+ int[] stops = this.mStops;
+ for (int i = 0; i < ns; ++i) {
+ int stop = stops[i];
+ if (stop > h) {
+ return stop;
}
-
- // No need to take bitmap height into account here,
- // since it is scaled to match the text height.
}
-
- lastPos = pos + 1;
}
+ return nextDefaultStop(h, mIncrement);
}
- if (fm != null) {
- fm.ascent = ascent;
- fm.descent = descent;
- fm.top = top;
- fm.bottom = bottom;
+ public static float nextDefaultStop(float h, int inc) {
+ return ((int) ((h + inc) / inc)) * inc;
}
-
- if (hasTabs)
- TextUtils.recycle(buf);
-
- return width;
}
/**
@@ -1804,23 +1570,22 @@ public abstract class Layout {
/**
* Stores information about bidirectional (left-to-right or right-to-left)
- * text within the layout of a line. TODO: This work is not complete
- * or correct and will be fleshed out in a later revision.
+ * text within the layout of a line.
*/
public static class Directions {
- private short[] mDirections;
-
- // The values in mDirections are the offsets from the first character
- // in the line to the next flip in direction. Runs at even indices
- // are left-to-right, the others are right-to-left. So, for example,
- // a line that starts with a right-to-left run has 0 at mDirections[0],
- // since the 'first' (ltr) run is zero length.
- //
- // The code currently assumes that each run is adjacent to the previous
- // one, progressing in the base line direction. This isn't sufficient
- // to handle nested runs, for example numeric text in an rtl context
- // in an ltr paragraph.
- /* package */ Directions(short[] dirs) {
+ // Directions represents directional runs within a line of text.
+ // Runs are pairs of ints listed in visual order, starting from the
+ // leading margin. The first int of each pair is the offset from
+ // the first character of the line to the start of the run. The
+ // second int represents both the length and level of the run.
+ // The length is in the lower bits, accessed by masking with
+ // DIR_LENGTH_MASK. The level is in the higher bits, accessed
+ // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
+ // To simply test for an RTL direction, test the bit using
+ // DIR_RTL_FLAG, if set then the direction is rtl.
+
+ /* package */ int[] mDirections;
+ /* package */ Directions(int[] dirs) {
mDirections = dirs;
}
}
@@ -1831,6 +1596,7 @@ public abstract class Layout {
* line is ellipsized, not getLineStart().)
*/
public abstract int getEllipsisStart(int line);
+
/**
* Returns the number of characters to be ellipsized away, or 0 if
* no ellipsis is to take place.
@@ -1870,7 +1636,7 @@ public abstract class Layout {
public int length() {
return mText.length();
}
-
+
public CharSequence subSequence(int start, int end) {
char[] s = new char[end - start];
getChars(start, end, s, 0);
@@ -1936,12 +1702,17 @@ public abstract class Layout {
public static final int DIR_LEFT_TO_RIGHT = 1;
public static final int DIR_RIGHT_TO_LEFT = -1;
-
+
/* package */ static final int DIR_REQUEST_LTR = 1;
/* package */ static final int DIR_REQUEST_RTL = -1;
/* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
/* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
+ /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
+ /* package */ static final int RUN_LEVEL_SHIFT = 26;
+ /* package */ static final int RUN_LEVEL_MASK = 0x3f;
+ /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
+
public enum Alignment {
ALIGN_NORMAL,
ALIGN_OPPOSITE,
@@ -1953,9 +1724,7 @@ public abstract class Layout {
private static final int TAB_INCREMENT = 20;
/* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
- new Directions(new short[] { 32767 });
+ new Directions(new int[] { 0, RUN_LENGTH_MASK });
/* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
- new Directions(new short[] { 0, 32767 });
-
+ new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
}
-
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
new file mode 100644
index 0000000..d5699f1
--- /dev/null
+++ b/core/java/android/text/MeasuredText.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.util.ArrayUtils;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
+import android.util.Log;
+
+/**
+ * @hide
+ */
+class MeasuredText {
+ /* package */ CharSequence mText;
+ /* package */ int mTextStart;
+ /* package */ float[] mWidths;
+ /* package */ char[] mChars;
+ /* package */ byte[] mLevels;
+ /* package */ int mDir;
+ /* package */ boolean mEasy;
+ /* package */ int mLen;
+ private int mPos;
+ private TextPaint mWorkPaint;
+
+ private MeasuredText() {
+ mWorkPaint = new TextPaint();
+ }
+
+ private static MeasuredText[] cached = new MeasuredText[3];
+
+ /* package */
+ static MeasuredText obtain() {
+ MeasuredText mt;
+ synchronized (cached) {
+ for (int i = cached.length; --i >= 0;) {
+ if (cached[i] != null) {
+ mt = cached[i];
+ cached[i] = null;
+ return mt;
+ }
+ }
+ }
+ mt = new MeasuredText();
+ Log.e("MEAS", "new: " + mt);
+ return mt;
+ }
+
+ /* package */
+ static MeasuredText recycle(MeasuredText mt) {
+ mt.mText = null;
+ if (mt.mLen < 1000) {
+ synchronized(cached) {
+ for (int i = 0; i < cached.length; ++i) {
+ if (cached[i] == null) {
+ cached[i] = mt;
+ break;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Analyzes text for bidirectional runs. Allocates working buffers.
+ */
+ /* package */
+ void setPara(CharSequence text, int start, int end, int bidiRequest) {
+ mText = text;
+ mTextStart = start;
+
+ int len = end - start;
+ mLen = len;
+ mPos = 0;
+
+ if (mWidths == null || mWidths.length < len) {
+ mWidths = new float[ArrayUtils.idealFloatArraySize(len)];
+ }
+ if (mChars == null || mChars.length < len) {
+ mChars = new char[ArrayUtils.idealCharArraySize(len)];
+ }
+ TextUtils.getChars(text, start, end, mChars, 0);
+
+ if (text instanceof Spanned) {
+ Spanned spanned = (Spanned) text;
+ ReplacementSpan[] spans = spanned.getSpans(start, end,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int startInPara = spanned.getSpanStart(spans[i]) - start;
+ int endInPara = spanned.getSpanEnd(spans[i]) - start;
+ for (int j = startInPara; j < endInPara; j++) {
+ mChars[j] = '\uFFFC';
+ }
+ }
+ }
+
+ if (TextUtils.doesNotNeedBidi(mChars, 0, len)) {
+ mDir = Layout.DIR_LEFT_TO_RIGHT;
+ mEasy = true;
+ } else {
+ if (mLevels == null || mLevels.length < len) {
+ mLevels = new byte[ArrayUtils.idealByteArraySize(len)];
+ }
+ mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false);
+ mEasy = false;
+ }
+ }
+
+ float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
+ if (fm != null) {
+ paint.getFontMetricsInt(fm);
+ }
+
+ int p = mPos;
+ mPos = p + len;
+
+ if (mEasy) {
+ int flags = mDir == Layout.DIR_LEFT_TO_RIGHT
+ ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL;
+ return paint.getTextRunAdvances(mChars, p, len, p, len, flags, mWidths, p);
+ }
+
+ float totalAdvance = 0;
+ int level = mLevels[p];
+ for (int q = p, i = p + 1, e = p + len;; ++i) {
+ if (i == e || mLevels[i] != level) {
+ int flags = (level & 0x1) == 0 ? Canvas.DIRECTION_LTR : Canvas.DIRECTION_RTL;
+ totalAdvance +=
+ paint.getTextRunAdvances(mChars, q, i - q, q, i - q, flags, mWidths, q);
+ if (i == e) {
+ break;
+ }
+ q = i;
+ level = mLevels[i];
+ }
+ }
+ return totalAdvance;
+ }
+
+ float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
+ Paint.FontMetricsInt fm) {
+
+ TextPaint workPaint = mWorkPaint;
+ workPaint.set(paint);
+ // XXX paint should not have a baseline shift, but...
+ workPaint.baselineShift = 0;
+
+ ReplacementSpan replacement = null;
+ for (int i = 0; i < spans.length; i++) {
+ MetricAffectingSpan span = spans[i];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ } else {
+ span.updateMeasureState(workPaint);
+ }
+ }
+
+ float wid;
+ if (replacement == null) {
+ wid = addStyleRun(workPaint, len, fm);
+ } else {
+ // Use original text. Shouldn't matter.
+ wid = replacement.getSize(workPaint, mText, mTextStart + mPos,
+ mTextStart + mPos + len, fm);
+ float[] w = mWidths;
+ w[mPos] = wid;
+ for (int i = mPos + 1, e = mPos + len; i < e; i++)
+ w[i] = 0;
+ }
+
+ if (fm != null) {
+ if (workPaint.baselineShift < 0) {
+ fm.ascent += workPaint.baselineShift;
+ fm.top += workPaint.baselineShift;
+ } else {
+ fm.descent += workPaint.baselineShift;
+ fm.bottom += workPaint.baselineShift;
+ }
+ }
+
+ return wid;
+ }
+
+ int breakText(int start, int limit, boolean forwards, float width) {
+ float[] w = mWidths;
+ if (forwards) {
+ for (int i = start; i < limit; ++i) {
+ if ((width -= w[i]) < 0) {
+ return i - start;
+ }
+ }
+ } else {
+ for (int i = limit; --i >= start;) {
+ if ((width -= w[i]) < 0) {
+ return limit - i -1;
+ }
+ }
+ }
+
+ return limit - start;
+ }
+
+ float measure(int start, int limit) {
+ float width = 0;
+ float[] w = mWidths;
+ for (int i = start; i < limit; ++i) {
+ width += w[i];
+ }
+ return width;
+ }
+} \ No newline at end of file
diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java
index bb98bce..13cb5e6 100644
--- a/core/java/android/text/Selection.java
+++ b/core/java/android/text/Selection.java
@@ -417,8 +417,8 @@ public class Selection {
}
}
- private static final class START implements NoCopySpan { };
- private static final class END implements NoCopySpan { };
+ private static final class START implements NoCopySpan { }
+ private static final class END implements NoCopySpan { }
/*
* Public constants
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index caaafa1..fc01ef2 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -17,8 +17,9 @@
package android.text;
import com.android.internal.util.ArrayUtils;
-import android.graphics.Paint;
+
import android.graphics.Canvas;
+import android.graphics.Paint;
import java.lang.reflect.Array;
@@ -312,12 +313,15 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
moveGapTo(end);
- if (tbend - tbstart >= mGapLength + (end - start))
- resizeFor(mText.length - mGapLength +
- tbend - tbstart - (end - start));
+ // Can be negative
+ final int nbNewChars = (tbend - tbstart) - (end - start);
- mGapStart += tbend - tbstart - (end - start);
- mGapLength -= tbend - tbstart - (end - start);
+ if (nbNewChars >= mGapLength) {
+ resizeFor(mText.length + nbNewChars - mGapLength);
+ }
+
+ mGapStart += nbNewChars;
+ mGapLength -= nbNewChars;
if (mGapLength < 1)
new Exception("mGapLength < 1").printStackTrace();
@@ -707,6 +711,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
* the specified range of the buffer. The kind may be Object.class to get
* a list of all the spans regardless of type.
*/
+ @SuppressWarnings("unchecked")
public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
int spanCount = mSpanCount;
Object[] spans = mSpans;
@@ -717,8 +722,8 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
int gaplen = mGapLength;
int count = 0;
- Object[] ret = null;
- Object ret1 = null;
+ T[] ret = null;
+ T ret1 = null;
for (int i = 0; i < spanCount; i++) {
int spanStart = starts[i];
@@ -750,11 +755,13 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
}
if (count == 0) {
- ret1 = spans[i];
+ // Safe conversion thanks to the isInstance test above
+ ret1 = (T) spans[i];
count++;
} else {
if (count == 1) {
- ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
+ // Safe conversion, but requires a suppressWarning
+ ret = (T[]) Array.newInstance(kind, spanCount - i + 1);
ret[0] = ret1;
}
@@ -771,29 +778,33 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
}
System.arraycopy(ret, j, ret, j + 1, count - j);
- ret[j] = spans[i];
+ // Safe conversion thanks to the isInstance test above
+ ret[j] = (T) spans[i];
count++;
} else {
- ret[count++] = spans[i];
+ // Safe conversion thanks to the isInstance test above
+ ret[count++] = (T) spans[i];
}
}
}
if (count == 0) {
- return (T[]) ArrayUtils.emptyArray(kind);
+ return ArrayUtils.emptyArray(kind);
}
if (count == 1) {
- ret = (Object[]) Array.newInstance(kind, 1);
+ // Safe conversion, but requires a suppressWarning
+ ret = (T[]) Array.newInstance(kind, 1);
ret[0] = ret1;
- return (T[]) ret;
+ return ret;
}
if (count == ret.length) {
- return (T[]) ret;
+ return ret;
}
- Object[] nret = (Object[]) Array.newInstance(kind, count);
+ // Safe conversion, but requires a suppressWarning
+ T[] nret = (T[]) Array.newInstance(kind, count);
System.arraycopy(ret, 0, nret, 0, count);
- return (T[]) nret;
+ return nret;
}
/**
@@ -862,6 +873,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
/**
* Return a String containing a copy of the chars in this buffer.
*/
+ @Override
public String toString() {
int len = length();
char[] buf = new char[len];
@@ -952,6 +964,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
}
}
+/*
private boolean isprint(char c) { // XXX
if (c >= ' ' && c <= '~')
return true;
@@ -959,7 +972,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
return false;
}
-/*
private static final int startFlag(int flag) {
return (flag >> 4) & 0x0F;
}
@@ -1054,7 +1066,32 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
}
}
+
/**
+ * Don't call this yourself -- exists for Canvas to use internally.
+ * {@hide}
+ */
+ public void drawTextRun(Canvas c, int start, int end,
+ int contextStart, int contextEnd,
+ float x, float y, int flags, Paint p) {
+ checkRange("drawTextRun", start, end);
+
+ int contextLen = contextEnd - contextStart;
+ int len = end - start;
+ if (contextEnd <= mGapStart) {
+ c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, flags, p);
+ } else if (contextStart >= mGapStart) {
+ c.drawTextRun(mText, start + mGapLength, len, contextStart + mGapLength,
+ contextLen, x, y, flags, p);
+ } else {
+ char[] buf = TextUtils.obtain(contextLen);
+ getChars(contextStart, contextEnd, buf, 0);
+ c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, flags, p);
+ TextUtils.recycle(buf);
+ }
+ }
+
+ /**
* Don't call this yourself -- exists for Paint to use internally.
* {@hide}
*/
@@ -1103,6 +1140,58 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable,
return ret;
}
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags,
+ float[] advances, int advancesPos, Paint p) {
+
+ float ret;
+
+ int contextLen = contextEnd - contextStart;
+ int len = end - start;
+
+ if (end <= mGapStart) {
+ ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen,
+ flags, advances, advancesPos);
+ } else if (start >= mGapStart) {
+ ret = p.getTextRunAdvances(mText, start + mGapLength, len,
+ contextStart + mGapLength, contextLen, flags, advances, advancesPos);
+ } else {
+ char[] buf = TextUtils.obtain(contextLen);
+ getChars(contextStart, contextEnd, buf, 0);
+ ret = p.getTextRunAdvances(buf, start - contextStart, len,
+ 0, contextLen, flags, advances, advancesPos);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset,
+ int cursorOpt, Paint p) {
+
+ int ret;
+
+ int contextLen = contextEnd - contextStart;
+ if (contextEnd <= mGapStart) {
+ ret = p.getTextRunCursor(mText, contextStart, contextLen,
+ flags, offset, cursorOpt);
+ } else if (contextStart >= mGapStart) {
+ ret = p.getTextRunCursor(mText, contextStart + mGapLength, contextLen,
+ flags, offset + mGapLength, cursorOpt) - mGapLength;
+ } else {
+ char[] buf = TextUtils.obtain(contextLen);
+ getChars(contextStart, contextEnd, buf, 0);
+ ret = p.getTextRunCursor(buf, 0, contextLen,
+ flags, offset - contextStart, cursorOpt) + contextStart;
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
// Documentation from interface
public void setFilters(InputFilter[] filters) {
if (filters == null) {
diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java
index 154497d..d14fcbc 100644
--- a/core/java/android/text/Spanned.java
+++ b/core/java/android/text/Spanned.java
@@ -91,7 +91,7 @@ extends CharSequence
public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;
/**
- * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand
* to include text inserted at their ending point but not at their
* starting point. When 0-length, they behave like points.
*/
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index f02ad2a..44157de 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -16,14 +16,15 @@
package android.text;
+import com.android.internal.util.ArrayUtils;
+
import android.graphics.Bitmap;
import android.graphics.Paint;
-import com.android.internal.util.ArrayUtils;
-import android.util.Log;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineHeightSpan;
import android.text.style.MetricAffectingSpan;
-import android.text.style.ReplacementSpan;
+import android.text.style.TabStopSpan;
+import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
/**
* StaticLayout is a Layout for text that will not be edited after it
@@ -31,8 +32,9 @@ import android.text.style.ReplacementSpan;
* <p>This is used by widgets to control text layout. You should not need
* to use this class directly unless you are implementing your own widget
* or custom display object, or would be tempted to call
- * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
- * Canvas.drawText()} directly.</p>
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
+ * float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
*/
public class
StaticLayout
@@ -62,7 +64,7 @@ extends Layout
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
super((ellipsize == null)
- ? source
+ ? source
: (source instanceof Spanned)
? new SpannedEllipsizer(source)
: new Ellipsizer(source),
@@ -72,7 +74,7 @@ extends Layout
* This is annoying, but we can't refer to the layout until
* superclass construction is finished, and the superclass
* constructor wants the reference to the display text.
- *
+ *
* This will break if the superclass constructor ever actually
* cares about the content instead of just holding the reference.
*/
@@ -94,13 +96,13 @@ extends Layout
mLineDirections = new Directions[
ArrayUtils.idealIntArraySize(2 * mColumns)];
+ mMeasured = MeasuredText.obtain();
+
generate(source, bufstart, bufend, paint, outerwidth, align,
spacingmult, spacingadd, includepad, includepad,
ellipsize != null, ellipsizedWidth, ellipsize);
- mChdirs = null;
- mChs = null;
- mWidths = null;
+ mMeasured = MeasuredText.recycle(mMeasured);
mFontMetricsInt = null;
}
@@ -111,6 +113,7 @@ extends Layout
mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
mLineDirections = new Directions[
ArrayUtils.idealIntArraySize(2 * mColumns)];
+ mMeasured = MeasuredText.obtain();
}
/* package */ void generate(CharSequence source, int bufstart, int bufend,
@@ -128,59 +131,50 @@ extends Layout
Paint.FontMetricsInt fm = mFontMetricsInt;
int[] choosehtv = null;
- int end = TextUtils.indexOf(source, '\n', bufstart, bufend);
- int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart;
- boolean first = true;
-
- if (mChdirs == null) {
- mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)];
- mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)];
- mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)];
- }
-
- byte[] chdirs = mChdirs;
- char[] chs = mChs;
- float[] widths = mWidths;
+ MeasuredText measured = mMeasured;
- AlteredCharSequence alter = null;
Spanned spanned = null;
-
if (source instanceof Spanned)
spanned = (Spanned) source;
int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
- for (int start = bufstart; start <= bufend; start = end) {
- if (first)
- first = false;
- else
- end = TextUtils.indexOf(source, '\n', start, bufend);
-
- if (end < 0)
- end = bufend;
+ int paraEnd;
+ for (int paraStart = bufstart; paraStart <= bufend; paraStart = paraEnd) {
+ paraEnd = TextUtils.indexOf(source, '\n', paraStart, bufend);
+ if (paraEnd < 0)
+ paraEnd = bufend;
else
- end++;
+ paraEnd++;
+ int paraLen = paraEnd - paraStart;
- int firstWidthLineCount = 1;
+ int firstWidthLineLimit = mLineCount + 1;
int firstwidth = outerwidth;
int restwidth = outerwidth;
LineHeightSpan[] chooseht = null;
if (spanned != null) {
- LeadingMarginSpan[] sp;
-
- sp = spanned.getSpans(start, end, LeadingMarginSpan.class);
+ LeadingMarginSpan[] sp = spanned.getSpans(paraStart, paraEnd,
+ LeadingMarginSpan.class);
for (int i = 0; i < sp.length; i++) {
LeadingMarginSpan lms = sp[i];
firstwidth -= sp[i].getLeadingMargin(true);
restwidth -= sp[i].getLeadingMargin(false);
- if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) {
- firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount();
+
+ // LeadingMarginSpan2 is odd. The count affects all
+ // leading margin spans, not just this particular one,
+ // and start from the top of the span, not the top of the
+ // paragraph.
+ if (lms instanceof LeadingMarginSpan2) {
+ LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
+ int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2));
+ firstWidthLineLimit = lmsFirstLine +
+ lms2.getLeadingMarginLineCount();
}
}
- chooseht = spanned.getSpans(start, end, LineHeightSpan.class);
+ chooseht = spanned.getSpans(paraStart, paraEnd, LineHeightSpan.class);
if (chooseht.length != 0) {
if (choosehtv == null ||
@@ -192,11 +186,11 @@ extends Layout
for (int i = 0; i < chooseht.length; i++) {
int o = spanned.getSpanStart(chooseht[i]);
- if (o < start) {
+ if (o < paraStart) {
// starts in this layout, before the
// current paragraph
- choosehtv[i] = getLineTop(getLineForOffset(o));
+ choosehtv[i] = getLineTop(getLineForOffset(o));
} else {
// starts in this paragraph
@@ -206,162 +200,87 @@ extends Layout
}
}
- if (end - start > chdirs.length) {
- chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)];
- mChdirs = chdirs;
- }
- if (end - start > chs.length) {
- chs = new char[ArrayUtils.idealCharArraySize(end - start)];
- mChs = chs;
- }
- if ((end - start) * 2 > widths.length) {
- widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)];
- mWidths = widths;
- }
-
- TextUtils.getChars(source, start, end, chs, 0);
- final int n = end - start;
-
- boolean easy = true;
- boolean altered = false;
- int dir = DEFAULT_DIR; // XXX
-
- for (int i = 0; i < n; i++) {
- if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
- easy = false;
- break;
- }
- }
+ measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR);
+ char[] chs = measured.mChars;
+ float[] widths = measured.mWidths;
+ byte[] chdirs = measured.mLevels;
+ int dir = measured.mDir;
+ boolean easy = measured.mEasy;
- // Ensure that none of the underlying characters are treated
- // as viable breakpoints, and that the entire run gets the
- // same bidi direction.
-
- if (source instanceof Spanned) {
- Spanned sp = (Spanned) source;
- ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class);
-
- for (int y = 0; y < spans.length; y++) {
- int a = sp.getSpanStart(spans[y]);
- int b = sp.getSpanEnd(spans[y]);
-
- for (int x = a; x < b; x++) {
- chs[x - start] = '\uFFFC';
- }
- }
- }
-
- if (!easy) {
- // XXX put override flags, etc. into chdirs
- dir = bidi(dir, chs, chdirs, n, false);
-
- // Do mirroring for right-to-left segments
-
- for (int i = 0; i < n; i++) {
- if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
- int j;
-
- for (j = i; j < n; j++) {
- if (chdirs[j] !=
- Character.DIRECTIONALITY_RIGHT_TO_LEFT)
- break;
- }
-
- if (AndroidCharacter.mirror(chs, i, j - i))
- altered = true;
-
- i = j - 1;
- }
- }
- }
-
- CharSequence sub;
-
- if (altered) {
- if (alter == null)
- alter = AlteredCharSequence.make(source, chs, start, end);
- else
- alter.update(chs, start, end);
-
- sub = alter;
- } else {
- sub = source;
- }
+ CharSequence sub = source;
int width = firstwidth;
float w = 0;
- int here = start;
+ int here = paraStart;
- int ok = start;
+ int ok = paraStart;
float okwidth = w;
int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0;
- int fit = start;
+ int fit = paraStart;
float fitwidth = w;
int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0;
- boolean tab = false;
-
- int next;
- for (int i = start; i < end; i = next) {
- if (spanned == null)
- next = end;
- else
- next = spanned.nextSpanTransition(i, end,
- MetricAffectingSpan.
- class);
-
- if (spanned == null) {
- paint.getTextWidths(sub, i, next, widths);
- System.arraycopy(widths, 0, widths,
- end - start + (i - start), next - i);
-
- paint.getFontMetricsInt(fm);
- } else {
- mWorkPaint.baselineShift = 0;
+ boolean hasTabOrEmoji = false;
+ boolean hasTab = false;
+ TabStops tabStops = null;
+
+ for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart;
+ spanStart < paraEnd; spanStart = nextSpanStart) {
- Styled.getTextWidths(paint, mWorkPaint,
- spanned, i, next,
- widths, fm);
- System.arraycopy(widths, 0, widths,
- end - start + (i - start), next - i);
+ if (spanStart == spanEnd) {
+ if (spanned == null)
+ spanEnd = paraEnd;
+ else
+ spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
+ MetricAffectingSpan.class);
- if (mWorkPaint.baselineShift < 0) {
- fm.ascent += mWorkPaint.baselineShift;
- fm.top += mWorkPaint.baselineShift;
+ int spanLen = spanEnd - spanStart;
+ if (spanned == null) {
+ measured.addStyleRun(paint, spanLen, fm);
} else {
- fm.descent += mWorkPaint.baselineShift;
- fm.bottom += mWorkPaint.baselineShift;
+ MetricAffectingSpan[] spans =
+ spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
+ measured.addStyleRun(paint, spans, spanLen, fm);
}
}
+ nextSpanStart = spanEnd;
+ int startInPara = spanStart - paraStart;
+ int endInPara = spanEnd - paraStart;
+
int fmtop = fm.top;
int fmbottom = fm.bottom;
int fmascent = fm.ascent;
int fmdescent = fm.descent;
- if (false) {
- StringBuilder sb = new StringBuilder();
- for (int j = i; j < next; j++) {
- sb.append(widths[j - start + (end - start)]);
- sb.append(' ');
- }
-
- Log.e("text", sb.toString());
- }
-
- for (int j = i; j < next; j++) {
- char c = chs[j - start];
+ for (int j = spanStart; j < spanEnd; j++) {
+ char c = chs[j - paraStart];
float before = w;
if (c == '\n') {
;
} else if (c == '\t') {
- w = Layout.nextTab(sub, start, end, w, null);
- tab = true;
- } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < next) {
- int emoji = Character.codePointAt(chs, j - start);
+ if (hasTab == false) {
+ hasTab = true;
+ hasTabOrEmoji = true;
+ if (spanned != null) {
+ // First tab this para, check for tabstops
+ TabStopSpan[] spans = spanned.getSpans(paraStart,
+ paraEnd, TabStopSpan.class);
+ if (spans.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, spans);
+ }
+ }
+ }
+ if (tabStops != null) {
+ w = tabStops.nextTab(w);
+ } else {
+ w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
+ }
+ } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < spanEnd) {
+ int emoji = Character.codePointAt(chs, j - paraStart);
if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
Bitmap bm = EMOJI_FACTORY.
@@ -376,21 +295,21 @@ extends Layout
whichPaint = mWorkPaint;
}
- float wid = (float) bm.getWidth() *
+ float wid = bm.getWidth() *
-whichPaint.ascent() /
bm.getHeight();
w += wid;
- tab = true;
+ hasTabOrEmoji = true;
j++;
} else {
- w += widths[j - start + (end - start)];
+ w += widths[j - paraStart];
}
} else {
- w += widths[j - start + (end - start)];
+ w += widths[j - paraStart];
}
} else {
- w += widths[j - start + (end - start)];
+ w += widths[j - paraStart];
}
// Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
@@ -411,7 +330,7 @@ extends Layout
/*
* From the Unicode Line Breaking Algorithm:
* (at least approximately)
- *
+ *
* .,:; are class IS: breakpoints
* except when adjacent to digits
* / is class SY: a breakpoint
@@ -426,12 +345,12 @@ extends Layout
if (c == ' ' || c == '\t' ||
((c == '.' || c == ',' || c == ':' || c == ';') &&
- (j - 1 < here || !Character.isDigit(chs[j - 1 - start])) &&
- (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) ||
+ (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) &&
+ (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
((c == '/' || c == '-') &&
- (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) ||
+ (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
(c >= FIRST_CJK && isIdeographic(c, true) &&
- j + 1 < next && isIdeographic(chs[j + 1 - start], false))) {
+ j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) {
okwidth = w;
ok = j + 1;
@@ -448,7 +367,7 @@ extends Layout
if (ok != here) {
// Log.e("text", "output ok " + here + " to " +ok);
- while (ok < next && chs[ok - start] == ' ') {
+ while (ok < spanEnd && chs[ok - paraStart] == ' ') {
ok++;
}
@@ -457,10 +376,10 @@ extends Layout
okascent, okdescent, oktop, okbottom,
v,
spacingmult, spacingadd, chooseht,
- choosehtv, fm, tab,
- needMultiply, start, chdirs, dir, easy,
+ choosehtv, fm, hasTabOrEmoji,
+ needMultiply, paraStart, chdirs, dir, easy,
ok == bufend, includepad, trackpad,
- widths, start, end - start,
+ chs, widths, here - paraStart,
where, ellipsizedWidth, okwidth,
paint);
@@ -484,7 +403,7 @@ extends Layout
if (ok != here) {
// Log.e("text", "output ok " + here + " to " +ok);
- while (ok < next && chs[ok - start] == ' ') {
+ while (ok < spanEnd && chs[ok - paraStart] == ' ') {
ok++;
}
@@ -493,10 +412,10 @@ extends Layout
okascent, okdescent, oktop, okbottom,
v,
spacingmult, spacingadd, chooseht,
- choosehtv, fm, tab,
- needMultiply, start, chdirs, dir, easy,
+ choosehtv, fm, hasTabOrEmoji,
+ needMultiply, paraStart, chdirs, dir, easy,
ok == bufend, includepad, trackpad,
- widths, start, end - start,
+ chs, widths, here - paraStart,
where, ellipsizedWidth, okwidth,
paint);
@@ -509,19 +428,20 @@ extends Layout
fittop, fitbottom,
v,
spacingmult, spacingadd, chooseht,
- choosehtv, fm, tab,
- needMultiply, start, chdirs, dir, easy,
+ choosehtv, fm, hasTabOrEmoji,
+ needMultiply, paraStart, chdirs, dir, easy,
fit == bufend, includepad, trackpad,
- widths, start, end - start,
+ chs, widths, here - paraStart,
where, ellipsizedWidth, fitwidth,
paint);
here = fit;
} else {
// Log.e("text", "output one " + here + " to " +(here + 1));
- measureText(paint, mWorkPaint,
- source, here, here + 1, fm, tab,
- null);
+ // XXX not sure why the existing fm wasn't ok.
+ // measureText(paint, mWorkPaint,
+ // source, here, here + 1, fm, tab,
+ // null);
v = out(source,
here, here+1,
@@ -529,19 +449,22 @@ extends Layout
fm.top, fm.bottom,
v,
spacingmult, spacingadd, chooseht,
- choosehtv, fm, tab,
- needMultiply, start, chdirs, dir, easy,
+ choosehtv, fm, hasTabOrEmoji,
+ needMultiply, paraStart, chdirs, dir, easy,
here + 1 == bufend, includepad,
trackpad,
- widths, start, end - start,
+ chs, widths, here - paraStart,
where, ellipsizedWidth,
- widths[here - start], paint);
+ widths[here - paraStart], paint);
here = here + 1;
}
- if (here < i) {
- j = next = here; // must remeasure
+ if (here < spanStart) {
+ // didn't output all the text for this span
+ // we've measured the raw widths, though, so
+ // just reset the start point
+ j = nextSpanStart = here;
} else {
j = here - 1; // continue looping
}
@@ -551,14 +474,14 @@ extends Layout
fitascent = fitdescent = fittop = fitbottom = 0;
okascent = okdescent = oktop = okbottom = 0;
- if (--firstWidthLineCount <= 0) {
+ if (--firstWidthLineLimit <= 0) {
width = restwidth;
}
}
}
}
- if (end != here) {
+ if (paraEnd != here) {
if ((fittop | fitbottom | fitdescent | fitascent) == 0) {
paint.getFontMetricsInt(fm);
@@ -571,20 +494,20 @@ extends Layout
// Log.e("text", "output rest " + here + " to " + end);
v = out(source,
- here, end, fitascent, fitdescent,
+ here, paraEnd, fitascent, fitdescent,
fittop, fitbottom,
v,
spacingmult, spacingadd, chooseht,
- choosehtv, fm, tab,
- needMultiply, start, chdirs, dir, easy,
- end == bufend, includepad, trackpad,
- widths, start, end - start,
+ choosehtv, fm, hasTabOrEmoji,
+ needMultiply, paraStart, chdirs, dir, easy,
+ paraEnd == bufend, includepad, trackpad,
+ chs, widths, here - paraStart,
where, ellipsizedWidth, w, paint);
}
- start = end;
+ paraStart = paraEnd;
- if (end == bufend)
+ if (paraEnd == bufend)
break;
}
@@ -599,246 +522,13 @@ extends Layout
v,
spacingmult, spacingadd, null,
null, fm, false,
- needMultiply, bufend, chdirs, DEFAULT_DIR, true,
+ needMultiply, bufend, null, DEFAULT_DIR, true,
true, includepad, trackpad,
- widths, bufstart, 0,
+ null, null, bufstart,
where, ellipsizedWidth, 0, paint);
}
}
- /**
- * Runs the unicode bidi algorithm on the first n chars in chs, returning
- * the char dirs in chInfo and the base line direction of the first
- * paragraph.
- *
- * XXX change result from dirs to levels
- *
- * @param dir the direction flag, either DIR_REQUEST_LTR,
- * DIR_REQUEST_RTL, DIR_REQUEST_DEFAULT_LTR, or DIR_REQUEST_DEFAULT_RTL.
- * @param chs the text to examine
- * @param chInfo on input, if hasInfo is true, override and other flags
- * representing out-of-band embedding information. On output, the generated
- * dirs of the text.
- * @param n the length of the text/information in chs and chInfo
- * @param hasInfo true if chInfo has input information, otherwise the
- * input data in chInfo is ignored.
- * @return the resolved direction level of the first paragraph, either
- * DIR_LEFT_TO_RIGHT or DIR_RIGHT_TO_LEFT.
- */
- /* package */ static int bidi(int dir, char[] chs, byte[] chInfo, int n,
- boolean hasInfo) {
-
- AndroidCharacter.getDirectionalities(chs, chInfo, n);
-
- /*
- * Determine primary paragraph direction if not specified
- */
- if (dir != DIR_REQUEST_LTR && dir != DIR_REQUEST_RTL) {
- // set up default
- dir = dir >= 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT;
- for (int j = 0; j < n; j++) {
- int d = chInfo[j];
-
- if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
- dir = DIR_LEFT_TO_RIGHT;
- break;
- }
- if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
- dir = DIR_RIGHT_TO_LEFT;
- break;
- }
- }
- }
-
- final byte SOR = dir == DIR_LEFT_TO_RIGHT ?
- Character.DIRECTIONALITY_LEFT_TO_RIGHT :
- Character.DIRECTIONALITY_RIGHT_TO_LEFT;
-
- /*
- * XXX Explicit overrides should go here
- */
-
- /*
- * Weak type resolution
- */
-
- // dump(chdirs, n, "initial");
-
- // W1 non spacing marks
- for (int j = 0; j < n; j++) {
- if (chInfo[j] == Character.NON_SPACING_MARK) {
- if (j == 0)
- chInfo[j] = SOR;
- else
- chInfo[j] = chInfo[j - 1];
- }
- }
-
- // dump(chdirs, n, "W1");
-
- // W2 european numbers
- byte cur = SOR;
- for (int j = 0; j < n; j++) {
- byte d = chInfo[j];
-
- if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
- d == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
- d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
- cur = d;
- else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) {
- if (cur ==
- Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
- chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
- }
- }
-
- // dump(chdirs, n, "W2");
-
- // W3 arabic letters
- for (int j = 0; j < n; j++) {
- if (chInfo[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
- chInfo[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
- }
-
- // dump(chdirs, n, "W3");
-
- // W4 single separator between numbers
- for (int j = 1; j < n - 1; j++) {
- byte d = chInfo[j];
- byte prev = chInfo[j - 1];
- byte next = chInfo[j + 1];
-
- if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) {
- if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
- next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
- chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
- } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) {
- if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
- next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
- chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
- if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER &&
- next == Character.DIRECTIONALITY_ARABIC_NUMBER)
- chInfo[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
- }
- }
-
- // dump(chdirs, n, "W4");
-
- // W5 european number terminators
- boolean adjacent = false;
- for (int j = 0; j < n; j++) {
- byte d = chInfo[j];
-
- if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
- adjacent = true;
- else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent)
- chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
- else
- adjacent = false;
- }
-
- //dump(chdirs, n, "W5");
-
- // W5 european number terminators part 2,
- // W6 separators and terminators
- adjacent = false;
- for (int j = n - 1; j >= 0; j--) {
- byte d = chInfo[j];
-
- if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
- adjacent = true;
- else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) {
- if (adjacent)
- chInfo[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
- else
- chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
- }
- else {
- adjacent = false;
-
- if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR ||
- d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR ||
- d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR ||
- d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR)
- chInfo[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
- }
- }
-
- // dump(chdirs, n, "W6");
-
- // W7 strong direction of european numbers
- cur = SOR;
- for (int j = 0; j < n; j++) {
- byte d = chInfo[j];
-
- if (d == SOR ||
- d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
- d == Character.DIRECTIONALITY_RIGHT_TO_LEFT)
- cur = d;
-
- if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
- chInfo[j] = cur;
- }
-
- // dump(chdirs, n, "W7");
-
- // N1, N2 neutrals
- cur = SOR;
- for (int j = 0; j < n; j++) {
- byte d = chInfo[j];
-
- if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
- d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
- cur = d;
- } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
- d == Character.DIRECTIONALITY_ARABIC_NUMBER) {
- cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
- } else {
- byte dd = SOR;
- int k;
-
- for (k = j + 1; k < n; k++) {
- dd = chInfo[k];
-
- if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
- dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
- break;
- }
- if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
- dd == Character.DIRECTIONALITY_ARABIC_NUMBER) {
- dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
- break;
- }
- }
-
- for (int y = j; y < k; y++) {
- if (dd == cur)
- chInfo[y] = cur;
- else
- chInfo[y] = SOR;
- }
-
- j = k - 1;
- }
- }
-
- // dump(chdirs, n, "final");
-
- // extra: enforce that all tabs and surrogate characters go the
- // primary direction
- // TODO: actually do directions right for surrogates
-
- for (int j = 0; j < n; j++) {
- char c = chs[j];
-
- if (c == '\t' || (c >= 0xD800 && c <= 0xDFFF)) {
- chInfo[j] = SOR;
- }
- }
-
- return dir;
- }
-
private static final char FIRST_CJK = '\u2E80';
/**
* Returns true if the specified character is one of those specified
@@ -944,37 +634,15 @@ extends Layout
}
*/
- private static int getFit(TextPaint paint,
- TextPaint workPaint,
- CharSequence text, int start, int end,
- float wid) {
- int high = end + 1, low = start - 1, guess;
-
- while (high - low > 1) {
- guess = (high + low) / 2;
-
- if (measureText(paint, workPaint,
- text, start, guess, null, true, null) > wid)
- high = guess;
- else
- low = guess;
- }
-
- if (low < start)
- return start;
- else
- return low;
- }
-
private int out(CharSequence text, int start, int end,
int above, int below, int top, int bottom, int v,
float spacingmult, float spacingadd,
LineHeightSpan[] chooseht, int[] choosehtv,
- Paint.FontMetricsInt fm, boolean tab,
+ Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
boolean needMultiply, int pstart, byte[] chdirs,
int dir, boolean easy, boolean last,
boolean includepad, boolean trackpad,
- float[] widths, int widstart, int widoff,
+ char[] chs, float[] widths, int widstart,
TextUtils.TruncateAt ellipsize, float ellipsiswidth,
float textwidth, TextPaint paint) {
int j = mLineCount;
@@ -982,8 +650,6 @@ extends Layout
int want = off + mColumns + TOP;
int[] lines = mLines;
- // Log.e("text", "line " + start + " to " + end + (last ? "===" : ""));
-
if (want >= lines.length) {
int nlen = ArrayUtils.idealIntArraySize(want + 1);
int[] grow = new int[nlen];
@@ -1059,59 +725,23 @@ extends Layout
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
- if (tab)
+ if (hasTabOrEmoji)
lines[off + TAB] |= TAB_MASK;
- {
- lines[off + DIR] |= dir << DIR_SHIFT;
-
- int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
- int count = 0;
-
- if (!easy) {
- for (int k = start; k < end; k++) {
- if (chdirs[k - pstart] != cur) {
- count++;
- cur = chdirs[k - pstart];
- }
- }
- }
-
- Directions linedirs;
-
- if (count == 0) {
- linedirs = DIRS_ALL_LEFT_TO_RIGHT;
- } else {
- short[] ld = new short[count + 1];
-
- cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
- count = 0;
- int here = start;
-
- for (int k = start; k < end; k++) {
- if (chdirs[k - pstart] != cur) {
- // XXX check to make sure we don't
- // overflow short
- ld[count++] = (short) (k - here);
- cur = chdirs[k - pstart];
- here = k;
- }
- }
-
- ld[count] = (short) (end - here);
-
- if (count == 1 && ld[0] == 0) {
- linedirs = DIRS_ALL_RIGHT_TO_LEFT;
- } else {
- linedirs = new Directions(ld);
- }
- }
-
+ lines[off + DIR] |= dir << DIR_SHIFT;
+ Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
+ // easy means all chars < the first RTL, so no emoji, no nothing
+ // XXX a run with no text or all spaces is easy but might be an empty
+ // RTL paragraph. Make sure easy is false if this is the case.
+ if (easy) {
mLineDirections[j] = linedirs;
+ } else {
+ mLineDirections[j] = AndroidBidi.directions(dir, chdirs, widstart, chs,
+ widstart, end - start);
// If ellipsize is in marquee mode, do not apply ellipsis on the first line
if (ellipsize != null && (ellipsize != TextUtils.TruncateAt.MARQUEE || j != 0)) {
- calculateEllipsis(start, end, widths, widstart, widoff,
+ calculateEllipsis(start, end, widths, widstart,
ellipsiswidth, ellipsize, j,
textwidth, paint);
}
@@ -1122,7 +752,7 @@ extends Layout
}
private void calculateEllipsis(int linestart, int lineend,
- float[] widths, int widstart, int widoff,
+ float[] widths, int widstart,
float avail, TextUtils.TruncateAt where,
int line, float textwidth, TextPaint paint) {
int len = lineend - linestart;
@@ -1142,7 +772,7 @@ extends Layout
int i;
for (i = len; i >= 0; i--) {
- float w = widths[i - 1 + linestart - widstart + widoff];
+ float w = widths[i - 1 + linestart - widstart];
if (w + sum + ellipsiswid > avail) {
break;
@@ -1158,7 +788,7 @@ extends Layout
int i;
for (i = 0; i < len; i++) {
- float w = widths[i + linestart - widstart + widoff];
+ float w = widths[i + linestart - widstart];
if (w + sum + ellipsiswid > avail) {
break;
@@ -1175,7 +805,7 @@ extends Layout
float ravail = (avail - ellipsiswid) / 2;
for (right = len; right >= 0; right--) {
- float w = widths[right - 1 + linestart - widstart + widoff];
+ float w = widths[right - 1 + linestart - widstart];
if (w + rsum > ravail) {
break;
@@ -1186,7 +816,7 @@ extends Layout
float lavail = avail - ellipsiswid - rsum;
for (left = 0; left < right; left++) {
- float w = widths[left + linestart - widstart + widoff];
+ float w = widths[left + linestart - widstart];
if (w + lsum > lavail) {
break;
@@ -1203,7 +833,7 @@ extends Layout
mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
}
- // Override the baseclass so we can directly access our members,
+ // Override the base class so we can directly access our members,
// rather than relying on member functions.
// The logic mirrors that of Layout.getLineForVertical
// FIXME: It may be faster to do a linear search for layouts without many lines.
@@ -1232,11 +862,11 @@ extends Layout
}
public int getLineTop(int line) {
- return mLines[mColumns * line + TOP];
+ return mLines[mColumns * line + TOP];
}
public int getLineDescent(int line) {
- return mLines[mColumns * line + DESCENT];
+ return mLines[mColumns * line + DESCENT];
}
public int getLineStart(int line) {
@@ -1309,13 +939,11 @@ extends Layout
private static final int DIR_SHIFT = 30;
private static final int TAB_MASK = 0x20000000;
- private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+ private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
/*
- * These are reused across calls to generate()
+ * This is reused across calls to generate()
*/
- private byte[] mChdirs;
- private char[] mChs;
- private float[] mWidths;
+ private MeasuredText mMeasured;
private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
}
diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java
deleted file mode 100644
index 513b2cd..0000000
--- a/core/java/android/text/Styled.java
+++ /dev/null
@@ -1,434 +0,0 @@
-/*
- * Copyright (C) 2006 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.text;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.text.style.CharacterStyle;
-import android.text.style.MetricAffectingSpan;
-import android.text.style.ReplacementSpan;
-
-/**
- * This class provides static methods for drawing and measuring styled text,
- * like {@link android.text.Spanned} object with
- * {@link android.text.style.ReplacementSpan}.
- *
- * @hide
- */
-public class Styled
-{
- /**
- * Draws and/or measures a uniform run of text on a single line. No span of
- * interest should start or end in the middle of this run (if not
- * drawing, character spans that don't affect metrics can be ignored).
- * Neither should the run direction change in the middle of the run.
- *
- * <p>The x position is the leading edge of the text. In a right-to-left
- * paragraph, this will be to the right of the text to be drawn. Paint
- * should not have an Align value other than LEFT or positioning will get
- * confused.
- *
- * <p>On return, workPaint will reflect the original paint plus any
- * modifications made by character styles on the run.
- *
- * <p>The returned width is signed and will be < 0 if the paragraph
- * direction is right-to-left.
- */
- private static float drawUniformRun(Canvas canvas,
- Spanned text, int start, int end,
- int dir, boolean runIsRtl,
- float x, int top, int y, int bottom,
- Paint.FontMetricsInt fmi,
- TextPaint paint,
- TextPaint workPaint,
- boolean needWidth) {
-
- boolean haveWidth = false;
- float ret = 0;
- CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class);
-
- ReplacementSpan replacement = null;
-
- // XXX: This shouldn't be modifying paint, only workPaint.
- // However, the members belonging to TextPaint should have default
- // values anyway. Better to ensure this in the Layout constructor.
- paint.bgColor = 0;
- paint.baselineShift = 0;
- workPaint.set(paint);
-
- if (spans.length > 0) {
- for (int i = 0; i < spans.length; i++) {
- CharacterStyle span = spans[i];
-
- if (span instanceof ReplacementSpan) {
- replacement = (ReplacementSpan)span;
- }
- else {
- span.updateDrawState(workPaint);
- }
- }
- }
-
- if (replacement == null) {
- CharSequence tmp;
- int tmpstart, tmpend;
-
- if (runIsRtl) {
- tmp = TextUtils.getReverse(text, start, end);
- tmpstart = 0;
- // XXX: assumes getReverse doesn't change the length of the text
- tmpend = end - start;
- } else {
- tmp = text;
- tmpstart = start;
- tmpend = end;
- }
-
- if (fmi != null) {
- workPaint.getFontMetricsInt(fmi);
- }
-
- if (canvas != null) {
- if (workPaint.bgColor != 0) {
- int c = workPaint.getColor();
- Paint.Style s = workPaint.getStyle();
- workPaint.setColor(workPaint.bgColor);
- workPaint.setStyle(Paint.Style.FILL);
-
- if (!haveWidth) {
- ret = workPaint.measureText(tmp, tmpstart, tmpend);
- haveWidth = true;
- }
-
- if (dir == Layout.DIR_RIGHT_TO_LEFT)
- canvas.drawRect(x - ret, top, x, bottom, workPaint);
- else
- canvas.drawRect(x, top, x + ret, bottom, workPaint);
-
- workPaint.setStyle(s);
- workPaint.setColor(c);
- }
-
- if (dir == Layout.DIR_RIGHT_TO_LEFT) {
- if (!haveWidth) {
- ret = workPaint.measureText(tmp, tmpstart, tmpend);
- haveWidth = true;
- }
-
- canvas.drawText(tmp, tmpstart, tmpend,
- x - ret, y + workPaint.baselineShift, workPaint);
- } else {
- if (needWidth) {
- if (!haveWidth) {
- ret = workPaint.measureText(tmp, tmpstart, tmpend);
- haveWidth = true;
- }
- }
-
- canvas.drawText(tmp, tmpstart, tmpend,
- x, y + workPaint.baselineShift, workPaint);
- }
- } else {
- if (needWidth && !haveWidth) {
- ret = workPaint.measureText(tmp, tmpstart, tmpend);
- haveWidth = true;
- }
- }
- } else {
- ret = replacement.getSize(workPaint, text, start, end, fmi);
-
- if (canvas != null) {
- if (dir == Layout.DIR_RIGHT_TO_LEFT)
- replacement.draw(canvas, text, start, end,
- x - ret, top, y, bottom, workPaint);
- else
- replacement.draw(canvas, text, start, end,
- x, top, y, bottom, workPaint);
- }
- }
-
- if (dir == Layout.DIR_RIGHT_TO_LEFT)
- return -ret;
- else
- return ret;
- }
-
- /**
- * Returns the advance widths for a uniform left-to-right run of text with
- * no style changes in the middle of the run. If any style is replacement
- * text, the first character will get the width of the replacement and the
- * remaining characters will get a width of 0.
- *
- * @param paint the paint, will not be modified
- * @param workPaint a paint to modify; on return will reflect the original
- * paint plus the effect of all spans on the run
- * @param text the text
- * @param start the start of the run
- * @param end the limit of the run
- * @param widths array to receive the advance widths of the characters. Must
- * be at least a large as (end - start).
- * @param fmi FontMetrics information; can be null
- * @return the actual number of widths returned
- */
- public static int getTextWidths(TextPaint paint,
- TextPaint workPaint,
- Spanned text, int start, int end,
- float[] widths, Paint.FontMetricsInt fmi) {
- MetricAffectingSpan[] spans =
- text.getSpans(start, end, MetricAffectingSpan.class);
-
- ReplacementSpan replacement = null;
- workPaint.set(paint);
-
- for (int i = 0; i < spans.length; i++) {
- MetricAffectingSpan span = spans[i];
- if (span instanceof ReplacementSpan) {
- replacement = (ReplacementSpan)span;
- }
- else {
- span.updateMeasureState(workPaint);
- }
- }
-
- if (replacement == null) {
- workPaint.getFontMetricsInt(fmi);
- workPaint.getTextWidths(text, start, end, widths);
- } else {
- int wid = replacement.getSize(workPaint, text, start, end, fmi);
-
- if (end > start) {
- widths[0] = wid;
- for (int i = start + 1; i < end; i++)
- widths[i - start] = 0;
- }
- }
- return end - start;
- }
-
- /**
- * Renders and/or measures a directional run of text on a single line.
- * Unlike {@link #drawUniformRun}, this can render runs that cross style
- * boundaries. Returns the signed advance width, if requested.
- *
- * <p>The x position is the leading edge of the text. In a right-to-left
- * paragraph, this will be to the right of the text to be drawn. Paint
- * should not have an Align value other than LEFT or positioning will get
- * confused.
- *
- * <p>This optimizes for unstyled text and so workPaint might not be
- * modified by this call.
- *
- * <p>The returned advance width will be < 0 if the paragraph
- * direction is right-to-left.
- */
- private static float drawDirectionalRun(Canvas canvas,
- CharSequence text, int start, int end,
- int dir, boolean runIsRtl,
- float x, int top, int y, int bottom,
- Paint.FontMetricsInt fmi,
- TextPaint paint,
- TextPaint workPaint,
- boolean needWidth) {
-
- // XXX: It looks like all calls to this API match dir and runIsRtl, so
- // having both parameters is redundant and confusing.
-
- // fast path for unstyled text
- if (!(text instanceof Spanned)) {
- float ret = 0;
-
- if (runIsRtl) {
- CharSequence tmp = TextUtils.getReverse(text, start, end);
- // XXX: this assumes getReverse doesn't tweak the length of
- // the text
- int tmpend = end - start;
-
- if (canvas != null || needWidth)
- ret = paint.measureText(tmp, 0, tmpend);
-
- if (canvas != null)
- canvas.drawText(tmp, 0, tmpend,
- x - ret, y, paint);
- } else {
- if (needWidth)
- ret = paint.measureText(text, start, end);
-
- if (canvas != null)
- canvas.drawText(text, start, end, x, y, paint);
- }
-
- if (fmi != null) {
- paint.getFontMetricsInt(fmi);
- }
-
- return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1
- }
-
- float ox = x;
- int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0;
-
- Spanned sp = (Spanned) text;
- Class<?> division;
-
- if (canvas == null)
- division = MetricAffectingSpan.class;
- else
- division = CharacterStyle.class;
-
- int next;
- for (int i = start; i < end; i = next) {
- next = sp.nextSpanTransition(i, end, division);
-
- // XXX: if dir and runIsRtl were not the same, this would draw
- // spans in the wrong order, but no one appears to call it this
- // way.
- x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl,
- x, top, y, bottom, fmi, paint, workPaint,
- needWidth || next != end);
-
- if (fmi != null) {
- if (fmi.ascent < minAscent)
- minAscent = fmi.ascent;
- if (fmi.descent > maxDescent)
- maxDescent = fmi.descent;
-
- if (fmi.top < minTop)
- minTop = fmi.top;
- if (fmi.bottom > maxBottom)
- maxBottom = fmi.bottom;
- }
- }
-
- if (fmi != null) {
- if (start == end) {
- paint.getFontMetricsInt(fmi);
- } else {
- fmi.ascent = minAscent;
- fmi.descent = maxDescent;
- fmi.top = minTop;
- fmi.bottom = maxBottom;
- }
- }
-
- return x - ox;
- }
-
- /**
- * Draws a unidirectional run of text on a single line, and optionally
- * returns the signed advance. Unlike drawDirectionalRun, the paragraph
- * direction and run direction can be different.
- */
- /* package */ static float drawText(Canvas canvas,
- CharSequence text, int start, int end,
- int dir, boolean runIsRtl,
- float x, int top, int y, int bottom,
- TextPaint paint,
- TextPaint workPaint,
- boolean needWidth) {
- // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl
- if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) ||
- (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) {
- // TODO: this needs the real direction
- float ch = drawDirectionalRun(null, text, start, end,
- Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint,
- workPaint, true);
-
- ch *= dir; // DIR_RIGHT_TO_LEFT == -1
- drawDirectionalRun(canvas, text, start, end, -dir,
- runIsRtl, x + ch, top, y, bottom, null, paint,
- workPaint, true);
-
- return ch;
- }
-
- return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl,
- x, top, y, bottom, null, paint, workPaint,
- needWidth);
- }
-
- /**
- * Draws a run of text on a single line, with its
- * origin at (x,y), in the specified Paint. The origin is interpreted based
- * on the Align setting in the Paint.
- *
- * This method considers style information in the text (e.g. even when text
- * is an instance of {@link android.text.Spanned}, this method correctly
- * draws the text). See also
- * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float,
- * float, Paint)} and
- * {@link android.graphics.Canvas#drawRect(float, float, float, float,
- * Paint)}.
- *
- * @param canvas The target canvas
- * @param text The text to be drawn
- * @param start The index of the first character in text to draw
- * @param end (end - 1) is the index of the last character in text to draw
- * @param direction The direction of the text. This must be
- * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or
- * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}.
- * @param x The x-coordinate of origin for where to draw the text
- * @param top The top side of the rectangle to be drawn
- * @param y The y-coordinate of origin for where to draw the text
- * @param bottom The bottom side of the rectangle to be drawn
- * @param paint The main {@link TextPaint} object.
- * @param workPaint The {@link TextPaint} object used for temporal
- * workspace.
- * @param needWidth If true, this method returns the width of drawn text
- * @return Width of the drawn text if needWidth is true
- */
- public static float drawText(Canvas canvas,
- CharSequence text, int start, int end,
- int direction,
- float x, int top, int y, int bottom,
- TextPaint paint,
- TextPaint workPaint,
- boolean needWidth) {
- // For safety.
- direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT
- : Layout.DIR_RIGHT_TO_LEFT;
-
- // Hide runIsRtl parameter since it is meaningless for external
- // developers.
- // XXX: the runIsRtl probably ought to be the same as direction, then
- // this could draw rtl text.
- return drawText(canvas, text, start, end, direction, false,
- x, top, y, bottom, paint, workPaint, needWidth);
- }
-
- /**
- * Returns the width of a run of left-to-right text on a single line,
- * considering style information in the text (e.g. even when text is an
- * instance of {@link android.text.Spanned}, this method correctly measures
- * the width of the text).
- *
- * @param paint the main {@link TextPaint} object; will not be modified
- * @param workPaint the {@link TextPaint} object available for modification;
- * will not necessarily be used
- * @param text the text to measure
- * @param start the index of the first character to start measuring
- * @param end 1 beyond the index of the last character to measure
- * @param fmi FontMetrics information; can be null
- * @return The width of the text
- */
- public static float measureText(TextPaint paint,
- TextPaint workPaint,
- CharSequence text, int start, int end,
- Paint.FontMetricsInt fmi) {
- return drawDirectionalRun(null, text, start, end,
- Layout.DIR_LEFT_TO_RIGHT, false,
- 0, 0, 0, 0, fmi, paint, workPaint, true);
- }
-}
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
new file mode 100644
index 0000000..0e3522e
--- /dev/null
+++ b/core/java/android/text/TextLine.java
@@ -0,0 +1,940 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.util.ArrayUtils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.RectF;
+import android.text.Layout.Directions;
+import android.text.Layout.TabStops;
+import android.text.style.CharacterStyle;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
+import android.util.Log;
+
+/**
+ * Represents a line of styled text, for measuring in visual order and
+ * for rendering.
+ *
+ * <p>Get a new instance using obtain(), and when finished with it, return it
+ * to the pool using recycle().
+ *
+ * <p>Call set to prepare the instance for use, then either draw, measure,
+ * metrics, or caretToLeftRightOf.
+ *
+ * @hide
+ */
+class TextLine {
+ private TextPaint mPaint;
+ private CharSequence mText;
+ private int mStart;
+ private int mLen;
+ private int mDir;
+ private Directions mDirections;
+ private boolean mHasTabs;
+ private TabStops mTabs;
+ private char[] mChars;
+ private boolean mCharsValid;
+ private Spanned mSpanned;
+ private final TextPaint mWorkPaint = new TextPaint();
+
+ private static TextLine[] cached = new TextLine[3];
+
+ /**
+ * Returns a new TextLine from the shared pool.
+ *
+ * @return an uninitialized TextLine
+ */
+ static TextLine obtain() {
+ TextLine tl;
+ synchronized (cached) {
+ for (int i = cached.length; --i >= 0;) {
+ if (cached[i] != null) {
+ tl = cached[i];
+ cached[i] = null;
+ return tl;
+ }
+ }
+ }
+ tl = new TextLine();
+ Log.e("TLINE", "new: " + tl);
+ return tl;
+ }
+
+ /**
+ * Puts a TextLine back into the shared pool. Do not use this TextLine once
+ * it has been returned.
+ * @param tl the textLine
+ * @return null, as a convenience from clearing references to the provided
+ * TextLine
+ */
+ static TextLine recycle(TextLine tl) {
+ tl.mText = null;
+ tl.mPaint = null;
+ tl.mDirections = null;
+ if (tl.mLen < 250) {
+ synchronized(cached) {
+ for (int i = 0; i < cached.length; ++i) {
+ if (cached[i] == null) {
+ cached[i] = tl;
+ break;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Initializes a TextLine and prepares it for use.
+ *
+ * @param paint the base paint for the line
+ * @param text the text, can be Styled
+ * @param start the start of the line relative to the text
+ * @param limit the limit of the line relative to the text
+ * @param dir the paragraph direction of this line
+ * @param directions the directions information of this line
+ * @param hasTabs true if the line might contain tabs or emoji
+ * @param tabStops the tabStops. Can be null.
+ */
+ void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
+ Directions directions, boolean hasTabs, TabStops tabStops) {
+ mPaint = paint;
+ mText = text;
+ mStart = start;
+ mLen = limit - start;
+ mDir = dir;
+ mDirections = directions;
+ mHasTabs = hasTabs;
+ mSpanned = null;
+
+ boolean hasReplacement = false;
+ if (text instanceof Spanned) {
+ mSpanned = (Spanned) text;
+ hasReplacement = mSpanned.getSpans(start, limit,
+ ReplacementSpan.class).length > 0;
+ }
+
+ mCharsValid = hasReplacement || hasTabs ||
+ directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
+
+ if (mCharsValid) {
+ if (mChars == null || mChars.length < mLen) {
+ mChars = new char[ArrayUtils.idealCharArraySize(mLen)];
+ }
+ TextUtils.getChars(text, start, limit, mChars, 0);
+ if (hasReplacement) {
+ // Handle these all at once so we don't have to do it as we go.
+ // Replace the first character of each replacement run with the
+ // object-replacement character and the remainder with zero width
+ // non-break space aka BOM. Cursor movement code skips these
+ // zero-width characters.
+ char[] chars = mChars;
+ for (int i = start, inext; i < limit; i = inext) {
+ inext = mSpanned.nextSpanTransition(i, limit,
+ ReplacementSpan.class);
+ if (mSpanned.getSpans(i, inext, ReplacementSpan.class)
+ .length > 0) { // transition into a span
+ chars[i - start] = '\ufffc';
+ for (int j = i - start + 1, e = inext - start; j < e; ++j) {
+ chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
+ }
+ }
+ }
+ }
+ }
+ mTabs = tabStops;
+ }
+
+ /**
+ * Renders the TextLine.
+ *
+ * @param c the canvas to render on
+ * @param x the leading margin position
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ */
+ void draw(Canvas c, float x, int top, int y, int bottom) {
+ if (!mHasTabs) {
+ if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
+ drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false);
+ return;
+ }
+ if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
+ drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false);
+ return;
+ }
+ }
+
+ float h = 0;
+ int[] runs = mDirections.mDirections;
+ RectF emojiRect = null;
+
+ int lastRunIndex = runs.length - 2;
+ for (int i = 0; i < runs.length; i += 2) {
+ int runStart = runs[i];
+ int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
+ if (runLimit > mLen) {
+ runLimit = mLen;
+ }
+ boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
+
+ int segstart = runStart;
+ char[] chars = mChars;
+ for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
+ int codept = 0;
+ Bitmap bm = null;
+
+ if (mHasTabs && j < runLimit) {
+ codept = mChars[j];
+ if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
+ codept = Character.codePointAt(mChars, j);
+ if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
+ bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
+ } else if (codept > 0xffff) {
+ ++j;
+ continue;
+ }
+ }
+ }
+
+ if (j == runLimit || codept == '\t' || bm != null) {
+ h += drawRun(c, i, segstart, j, runIsRtl, x+h, top, y, bottom,
+ i != lastRunIndex || j != mLen);
+
+ if (codept == '\t') {
+ h = mDir * nextTab(h * mDir);
+ } else if (bm != null) {
+ float bmAscent = ascent(j);
+ float bitmapHeight = bm.getHeight();
+ float scale = -bmAscent / bitmapHeight;
+ float width = bm.getWidth() * scale;
+
+ if (emojiRect == null) {
+ emojiRect = new RectF();
+ }
+ emojiRect.set(x + h, y + bmAscent,
+ x + h + width, y);
+ c.drawBitmap(bm, null, emojiRect, mPaint);
+ h += width;
+ j++;
+ }
+ segstart = j + 1;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns metrics information for the entire line.
+ *
+ * @param fmi receives font metrics information, can be null
+ * @return the signed width of the line
+ */
+ float metrics(FontMetricsInt fmi) {
+ return measure(mLen, false, fmi);
+ }
+
+ /**
+ * Returns information about a position on the line.
+ *
+ * @param offset the line-relative character offset, between 0 and the
+ * line length, inclusive
+ * @param trailing true to measure the trailing edge of the character
+ * before offset, false to measure the leading edge of the character
+ * at offset.
+ * @param fmi receives metrics information about the requested
+ * character, can be null.
+ * @return the signed offset from the leading margin to the requested
+ * character edge.
+ */
+ float measure(int offset, boolean trailing, FontMetricsInt fmi) {
+ int target = trailing ? offset - 1 : offset;
+ if (target < 0) {
+ return 0;
+ }
+
+ float h = 0;
+
+ if (!mHasTabs) {
+ if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
+ return measureRun(0, 0, offset, mLen, false, fmi);
+ }
+ if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
+ return measureRun(0, 0, offset, mLen, true, fmi);
+ }
+ }
+
+ char[] chars = mChars;
+ int[] runs = mDirections.mDirections;
+ for (int i = 0; i < runs.length; i += 2) {
+ int runStart = runs[i];
+ int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
+ if (runLimit > mLen) {
+ runLimit = mLen;
+ }
+ boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
+
+ int segstart = runStart;
+ for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
+ int codept = 0;
+ Bitmap bm = null;
+
+ if (mHasTabs && j < runLimit) {
+ codept = chars[j];
+ if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
+ codept = Character.codePointAt(chars, j);
+ if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
+ bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
+ } else if (codept > 0xffff) {
+ ++j;
+ continue;
+ }
+ }
+ }
+
+ if (j == runLimit || codept == '\t' || bm != null) {
+ boolean inSegment = target >= segstart && target < j;
+
+ boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
+ if (inSegment && advance) {
+ return h += measureRun(i, segstart, offset, j, runIsRtl, fmi);
+ }
+
+ float w = measureRun(i, segstart, j, j, runIsRtl, fmi);
+ h += advance ? w : -w;
+
+ if (inSegment) {
+ return h += measureRun(i, segstart, offset, j, runIsRtl, null);
+ }
+
+ if (codept == '\t') {
+ if (offset == j) {
+ return h;
+ }
+ h = mDir * nextTab(h * mDir);
+ if (target == j) {
+ return h;
+ }
+ }
+
+ if (bm != null) {
+ float bmAscent = ascent(j);
+ float wid = bm.getWidth() * -bmAscent / bm.getHeight();
+ h += mDir * wid;
+ j++;
+ }
+
+ segstart = j + 1;
+ }
+ }
+ }
+
+ return h;
+ }
+
+ /**
+ * Draws a unidirectional (but possibly multi-styled) run of text.
+ *
+ * @param c the canvas to draw on
+ * @param runIndex the index of this directional run
+ * @param start the line-relative start
+ * @param limit the line-relative limit
+ * @param runIsRtl true if the run is right-to-left
+ * @param x the position of the run that is closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param needWidth true if the width value is required.
+ * @return the signed width of the run, based on the paragraph direction.
+ * Only valid if needWidth is true.
+ */
+ private float drawRun(Canvas c, int runIndex, int start,
+ int limit, boolean runIsRtl, float x, int top, int y, int bottom,
+ boolean needWidth) {
+
+ if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
+ float w = -measureRun(runIndex, start, limit, limit, runIsRtl, null);
+ handleRun(runIndex, start, limit, limit, runIsRtl, c, x + w, top,
+ y, bottom, null, false);
+ return w;
+ }
+
+ return handleRun(runIndex, start, limit, limit, runIsRtl, c, x, top,
+ y, bottom, null, needWidth);
+ }
+
+ /**
+ * Measures a unidirectional (but possibly multi-styled) run of text.
+ *
+ * @param runIndex the run index
+ * @param start the line-relative start of the run
+ * @param offset the offset to measure to, between start and limit inclusive
+ * @param limit the line-relative limit of the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param fmi receives metrics information about the requested
+ * run, can be null.
+ * @return the signed width from the start of the run to the leading edge
+ * of the character at offset, based on the run (not paragraph) direction
+ */
+ private float measureRun(int runIndex, int start,
+ int offset, int limit, boolean runIsRtl, FontMetricsInt fmi) {
+ return handleRun(runIndex, start, offset, limit, runIsRtl, null,
+ 0, 0, 0, 0, fmi, true);
+ }
+
+ /**
+ * Walk the cursor through this line, skipping conjuncts and
+ * zero-width characters.
+ *
+ * <p>This function cannot properly walk the cursor off the ends of the line
+ * since it does not know about any shaping on the previous/following line
+ * that might affect the cursor position. Callers must either avoid these
+ * situations or handle the result specially.
+ *
+ * @param cursor the starting position of the cursor, between 0 and the
+ * length of the line, inclusive
+ * @param toLeft true if the caret is moving to the left.
+ * @return the new offset. If it is less than 0 or greater than the length
+ * of the line, the previous/following line should be examined to get the
+ * actual offset.
+ */
+ int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
+ // 1) The caret marks the leading edge of a character. The character
+ // logically before it might be on a different level, and the active caret
+ // position is on the character at the lower level. If that character
+ // was the previous character, the caret is on its trailing edge.
+ // 2) Take this character/edge and move it in the indicated direction.
+ // This gives you a new character and a new edge.
+ // 3) This position is between two visually adjacent characters. One of
+ // these might be at a lower level. The active position is on the
+ // character at the lower level.
+ // 4) If the active position is on the trailing edge of the character,
+ // the new caret position is the following logical character, else it
+ // is the character.
+
+ int lineStart = 0;
+ int lineEnd = mLen;
+ boolean paraIsRtl = mDir == -1;
+ int[] runs = mDirections.mDirections;
+
+ int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
+ boolean trailing = false;
+
+ if (cursor == lineStart) {
+ runIndex = -2;
+ } else if (cursor == lineEnd) {
+ runIndex = runs.length;
+ } else {
+ // First, get information about the run containing the character with
+ // the active caret.
+ for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
+ runStart = lineStart + runs[runIndex];
+ if (cursor >= runStart) {
+ runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
+ if (runLimit > lineEnd) {
+ runLimit = lineEnd;
+ }
+ if (cursor < runLimit) {
+ runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
+ Layout.RUN_LEVEL_MASK;
+ if (cursor == runStart) {
+ // The caret is on a run boundary, see if we should
+ // use the position on the trailing edge of the previous
+ // logical character instead.
+ int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
+ int pos = cursor - 1;
+ for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
+ prevRunStart = lineStart + runs[prevRunIndex];
+ if (pos >= prevRunStart) {
+ prevRunLimit = prevRunStart +
+ (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
+ if (prevRunLimit > lineEnd) {
+ prevRunLimit = lineEnd;
+ }
+ if (pos < prevRunLimit) {
+ prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
+ & Layout.RUN_LEVEL_MASK;
+ if (prevRunLevel < runLevel) {
+ // Start from logically previous character.
+ runIndex = prevRunIndex;
+ runLevel = prevRunLevel;
+ runStart = prevRunStart;
+ runLimit = prevRunLimit;
+ trailing = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // caret might be == lineEnd. This is generally a space or paragraph
+ // separator and has an associated run, but might be the end of
+ // text, in which case it doesn't. If that happens, we ran off the
+ // end of the run list, and runIndex == runs.length. In this case,
+ // we are at a run boundary so we skip the below test.
+ if (runIndex != runs.length) {
+ boolean runIsRtl = (runLevel & 0x1) != 0;
+ boolean advance = toLeft == runIsRtl;
+ if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
+ // Moving within or into the run, so we can move logically.
+ newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
+ runIsRtl, cursor, advance);
+ // If the new position is internal to the run, we're at the strong
+ // position already so we're finished.
+ if (newCaret != (advance ? runLimit : runStart)) {
+ return newCaret;
+ }
+ }
+ }
+ }
+
+ // If newCaret is -1, we're starting at a run boundary and crossing
+ // into another run. Otherwise we've arrived at a run boundary, and
+ // need to figure out which character to attach to. Note we might
+ // need to run this twice, if we cross a run boundary and end up at
+ // another run boundary.
+ while (true) {
+ boolean advance = toLeft == paraIsRtl;
+ int otherRunIndex = runIndex + (advance ? 2 : -2);
+ if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
+ int otherRunStart = lineStart + runs[otherRunIndex];
+ int otherRunLimit = otherRunStart +
+ (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
+ if (otherRunLimit > lineEnd) {
+ otherRunLimit = lineEnd;
+ }
+ int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
+ Layout.RUN_LEVEL_MASK;
+ boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
+
+ advance = toLeft == otherRunIsRtl;
+ if (newCaret == -1) {
+ newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
+ otherRunLimit, otherRunIsRtl,
+ advance ? otherRunStart : otherRunLimit, advance);
+ if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
+ // Crossed and ended up at a new boundary,
+ // repeat a second and final time.
+ runIndex = otherRunIndex;
+ runLevel = otherRunLevel;
+ continue;
+ }
+ break;
+ }
+
+ // The new caret is at a boundary.
+ if (otherRunLevel < runLevel) {
+ // The strong character is in the other run.
+ newCaret = advance ? otherRunStart : otherRunLimit;
+ }
+ break;
+ }
+
+ if (newCaret == -1) {
+ // We're walking off the end of the line. The paragraph
+ // level is always equal to or lower than any internal level, so
+ // the boundaries get the strong caret.
+ newCaret = advance ? mLen + 1 : -1;
+ break;
+ }
+
+ // Else we've arrived at the end of the line. That's a strong position.
+ // We might have arrived here by crossing over a run with no internal
+ // breaks and dropping out of the above loop before advancing one final
+ // time, so reset the caret.
+ // Note, we use '<=' below to handle a situation where the only run
+ // on the line is a counter-directional run. If we're not advancing,
+ // we can end up at the 'lineEnd' position but the caret we want is at
+ // the lineStart.
+ if (newCaret <= lineEnd) {
+ newCaret = advance ? lineEnd : lineStart;
+ }
+ break;
+ }
+
+ return newCaret;
+ }
+
+ /**
+ * Returns the next valid offset within this directional run, skipping
+ * conjuncts and zero-width characters. This should not be called to walk
+ * off the end of the line, since the returned values might not be valid
+ * on neighboring lines. If the returned offset is less than zero or
+ * greater than the line length, the offset should be recomputed on the
+ * preceding or following line, respectively.
+ *
+ * @param runIndex the run index
+ * @param runStart the start of the run
+ * @param runLimit the limit of the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param offset the offset
+ * @param after true if the new offset should logically follow the provided
+ * offset
+ * @return the new offset
+ */
+ private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
+ boolean runIsRtl, int offset, boolean after) {
+
+ if (runIndex < 0 || offset == (after ? mLen : 0)) {
+ // Walking off end of line. Since we don't know
+ // what cursor positions are available on other lines, we can't
+ // return accurate values. These are a guess.
+ if (after) {
+ return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
+ }
+ return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
+ }
+
+ TextPaint wp = mWorkPaint;
+ wp.set(mPaint);
+
+ int spanStart = runStart;
+ int spanLimit;
+ if (mSpanned == null) {
+ spanLimit = runLimit;
+ } else {
+ int target = after ? offset + 1 : offset;
+ int limit = mStart + runLimit;
+ while (true) {
+ spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
+ MetricAffectingSpan.class) - mStart;
+ if (spanLimit >= target) {
+ break;
+ }
+ spanStart = spanLimit;
+ }
+
+ MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
+ mStart + spanLimit, MetricAffectingSpan.class);
+
+ if (spans.length > 0) {
+ ReplacementSpan replacement = null;
+ for (int j = 0; j < spans.length; j++) {
+ MetricAffectingSpan span = spans[j];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ } else {
+ span.updateMeasureState(wp);
+ }
+ }
+
+ if (replacement != null) {
+ // If we have a replacement span, we're moving either to
+ // the start or end of this span.
+ return after ? spanLimit : spanStart;
+ }
+ }
+ }
+
+ int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
+ int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
+ if (mCharsValid) {
+ return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
+ flags, offset, cursorOpt);
+ } else {
+ return wp.getTextRunCursor(mText, mStart + spanStart,
+ mStart + spanLimit, flags, mStart + offset, cursorOpt) - mStart;
+ }
+ }
+
+ /**
+ * Utility function for measuring and rendering text. The text must
+ * not include a tab or emoji.
+ *
+ * @param wp the working paint
+ * @param start the start of the text
+ * @param end the end of the text
+ * @param runIsRtl true if the run is right-to-left
+ * @param c the canvas, can be null if rendering is not needed
+ * @param x the edge of the run closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param fmi receives metrics information, can be null
+ * @param needWidth true if the width of the run is needed
+ * @return the signed width of the run based on the run direction; only
+ * valid if needWidth is true
+ */
+ private float handleText(TextPaint wp, int start, int end,
+ int contextStart, int contextEnd, boolean runIsRtl,
+ Canvas c, float x, int top, int y, int bottom,
+ FontMetricsInt fmi, boolean needWidth) {
+
+ float ret = 0;
+
+ int runLen = end - start;
+ int contextLen = contextEnd - contextStart;
+ if (needWidth || (c != null && (wp.bgColor != 0 || runIsRtl))) {
+ int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
+ if (mCharsValid) {
+ ret = wp.getTextRunAdvances(mChars, start, runLen,
+ contextStart, contextLen, flags, null, 0);
+ } else {
+ int delta = mStart;
+ ret = wp.getTextRunAdvances(mText, delta + start,
+ delta + end, delta + contextStart, delta + contextEnd,
+ flags, null, 0);
+ }
+ }
+
+ if (fmi != null) {
+ wp.getFontMetricsInt(fmi);
+ }
+
+ if (c != null) {
+ if (runIsRtl) {
+ x -= ret;
+ }
+
+ if (wp.bgColor != 0) {
+ int color = wp.getColor();
+ Paint.Style s = wp.getStyle();
+ wp.setColor(wp.bgColor);
+ wp.setStyle(Paint.Style.FILL);
+
+ c.drawRect(x, top, x + ret, bottom, wp);
+
+ wp.setStyle(s);
+ wp.setColor(color);
+ }
+
+ drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
+ x, y + wp.baselineShift);
+ }
+
+ return runIsRtl ? -ret : ret;
+ }
+
+ /**
+ * Utility function for measuring and rendering a replacement.
+ *
+ * @param replacement the replacement
+ * @param wp the work paint
+ * @param runIndex the run index
+ * @param start the start of the run
+ * @param limit the limit of the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param c the canvas, can be null if not rendering
+ * @param x the edge of the replacement closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param fmi receives metrics information, can be null
+ * @param needWidth true if the width of the replacement is needed
+ * @return the signed width of the run based on the run direction; only
+ * valid if needWidth is true
+ */
+ private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
+ int runIndex, int start, int limit, boolean runIsRtl, Canvas c,
+ float x, int top, int y, int bottom, FontMetricsInt fmi,
+ boolean needWidth) {
+
+ float ret = 0;
+
+ int textStart = mStart + start;
+ int textLimit = mStart + limit;
+
+ if (needWidth || (c != null && runIsRtl)) {
+ ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
+ }
+
+ if (c != null) {
+ if (runIsRtl) {
+ x -= ret;
+ }
+ replacement.draw(c, mText, textStart, textLimit,
+ x, top, y, bottom, wp);
+ }
+
+ return runIsRtl ? -ret : ret;
+ }
+
+ /**
+ * Utility function for handling a unidirectional run. The run must not
+ * contain tabs or emoji but can contain styles.
+ *
+ * @param runIndex the run index
+ * @param start the line-relative start of the run
+ * @param measureLimit the offset to measure to, between start and limit inclusive
+ * @param limit the limit of the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param c the canvas, can be null
+ * @param x the end of the run closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param fmi receives metrics information, can be null
+ * @param needWidth true if the width is required
+ * @return the signed width of the run based on the run direction; only
+ * valid if needWidth is true
+ */
+ private float handleRun(int runIndex, int start, int measureLimit,
+ int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
+ int bottom, FontMetricsInt fmi, boolean needWidth) {
+
+ // Shaping needs to take into account context up to metric boundaries,
+ // but rendering needs to take into account character style boundaries.
+ // So we iterate through metric runs to get metric bounds,
+ // then within each metric run iterate through character style runs
+ // for the run bounds.
+ float ox = x;
+ for (int i = start, inext; i < measureLimit; i = inext) {
+ TextPaint wp = mWorkPaint;
+ wp.set(mPaint);
+
+ int mlimit;
+ if (mSpanned == null) {
+ inext = limit;
+ mlimit = measureLimit;
+ } else {
+ inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit,
+ MetricAffectingSpan.class) - mStart;
+
+ mlimit = inext < measureLimit ? inext : measureLimit;
+ MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i,
+ mStart + mlimit, MetricAffectingSpan.class);
+
+ if (spans.length > 0) {
+ ReplacementSpan replacement = null;
+ for (int j = 0; j < spans.length; j++) {
+ MetricAffectingSpan span = spans[j];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ } else {
+ // We might have a replacement that uses the draw
+ // state, otherwise measure state would suffice.
+ span.updateDrawState(wp);
+ }
+ }
+
+ if (replacement != null) {
+ x += handleReplacement(replacement, wp, runIndex, i,
+ mlimit, runIsRtl, c, x, top, y, bottom, fmi,
+ needWidth || mlimit < measureLimit);
+ continue;
+ }
+ }
+ }
+
+ if (mSpanned == null || c == null) {
+ x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
+ y, bottom, fmi, needWidth || mlimit < measureLimit);
+ } else {
+ for (int j = i, jnext; j < mlimit; j = jnext) {
+ jnext = mSpanned.nextSpanTransition(mStart + j,
+ mStart + mlimit, CharacterStyle.class) - mStart;
+
+ CharacterStyle[] spans = mSpanned.getSpans(mStart + j,
+ mStart + jnext, CharacterStyle.class);
+
+ wp.set(mPaint);
+ for (int k = 0; k < spans.length; k++) {
+ CharacterStyle span = spans[k];
+ span.updateDrawState(wp);
+ }
+
+ x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
+ top, y, bottom, fmi, needWidth || jnext < measureLimit);
+ }
+ }
+ }
+
+ return x - ox;
+ }
+
+ /**
+ * Render a text run with the set-up paint.
+ *
+ * @param c the canvas
+ * @param wp the paint used to render the text
+ * @param start the start of the run
+ * @param end the end of the run
+ * @param contextStart the start of context for the run
+ * @param contextEnd the end of the context for the run
+ * @param runIsRtl true if the run is right-to-left
+ * @param x the x position of the left edge of the run
+ * @param y the baseline of the run
+ */
+ private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
+ int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
+
+ int flags = runIsRtl ? Canvas.DIRECTION_RTL : Canvas.DIRECTION_LTR;
+ if (mCharsValid) {
+ int count = end - start;
+ int contextCount = contextEnd - contextStart;
+ c.drawTextRun(mChars, start, count, contextStart, contextCount,
+ x, y, flags, wp);
+ } else {
+ int delta = mStart;
+ c.drawTextRun(mText, delta + start, delta + end,
+ delta + contextStart, delta + contextEnd, x, y, flags, wp);
+ }
+ }
+
+ /**
+ * Returns the ascent of the text at start. This is used for scaling
+ * emoji.
+ *
+ * @param pos the line-relative position
+ * @return the ascent of the text at start
+ */
+ float ascent(int pos) {
+ if (mSpanned == null) {
+ return mPaint.ascent();
+ }
+
+ pos += mStart;
+ MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1,
+ MetricAffectingSpan.class);
+ if (spans.length == 0) {
+ return mPaint.ascent();
+ }
+
+ TextPaint wp = mWorkPaint;
+ wp.set(mPaint);
+ for (MetricAffectingSpan span : spans) {
+ span.updateMeasureState(wp);
+ }
+ return wp.ascent();
+ }
+
+ /**
+ * Returns the next tab position.
+ *
+ * @param h the (unsigned) offset from the leading margin
+ * @return the (unsigned) tab position after this offset
+ */
+ float nextTab(float h) {
+ if (mTabs != null) {
+ return mTabs.nextTab(h);
+ }
+ return TabStops.nextDefaultStop(h, TAB_INCREMENT);
+ }
+
+ private static final int TAB_INCREMENT = 20;
+}
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 9589bf3..2d6c7b6 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -17,12 +17,11 @@
package android.text;
import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
-import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.method.TextKeyListener.Capitalize;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
@@ -45,10 +44,8 @@ import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.Printer;
-import com.android.internal.util.ArrayUtils;
-
-import java.util.regex.Pattern;
import java.util.Iterator;
+import java.util.regex.Pattern;
public class TextUtils {
private TextUtils() { /* cannot be instantiated */ }
@@ -983,7 +980,7 @@ public class TextUtils {
/**
* Returns the original text if it fits in the specified width
* given the properties of the specified Paint,
- * or, if it does not fit, a copy with ellipsis character added
+ * or, if it does not fit, a copy with ellipsis character added
* at the specified edge or center.
* If <code>preserveLength</code> is specified, the returned copy
* will be padded with zero-width spaces to preserve the original
@@ -992,7 +989,7 @@ public class TextUtils {
* report the start and end of the ellipsized range.
*/
public static CharSequence ellipsize(CharSequence text,
- TextPaint p,
+ TextPaint paint,
float avail, TruncateAt where,
boolean preserveLength,
EllipsizeCallback callback) {
@@ -1003,13 +1000,12 @@ public class TextUtils {
int len = text.length();
- // Use Paint.breakText() for the non-Spanned case to avoid having
- // to allocate memory and accumulate the character widths ourselves.
-
- if (!(text instanceof Spanned)) {
- float wid = p.measureText(text, 0, len);
+ MeasuredText mt = MeasuredText.obtain();
+ try {
+ float width = setPara(mt, paint, text, 0, text.length(),
+ Layout.DIR_REQUEST_DEFAULT_LTR);
- if (wid <= avail) {
+ if (width <= avail) {
if (callback != null) {
callback.ellipsized(0, 0);
}
@@ -1017,252 +1013,71 @@ public class TextUtils {
return text;
}
- float ellipsiswid = p.measureText(sEllipsis);
-
- if (ellipsiswid > avail) {
- if (callback != null) {
- callback.ellipsized(0, len);
- }
-
- if (preserveLength) {
- char[] buf = obtain(len);
- for (int i = 0; i < len; i++) {
- buf[i] = '\uFEFF';
- }
- String ret = new String(buf, 0, len);
- recycle(buf);
- return ret;
- } else {
- return "";
- }
- }
-
- if (where == TruncateAt.START) {
- int fit = p.breakText(text, 0, len, false,
- avail - ellipsiswid, null);
-
- if (callback != null) {
- callback.ellipsized(0, len - fit);
- }
-
- if (preserveLength) {
- return blank(text, 0, len - fit);
- } else {
- return sEllipsis + text.toString().substring(len - fit, len);
- }
+ // XXX assumes ellipsis string does not require shaping and
+ // is unaffected by style
+ float ellipsiswid = paint.measureText(sEllipsis);
+ avail -= ellipsiswid;
+
+ int left = 0;
+ int right = len;
+ if (avail < 0) {
+ // it all goes
+ } else if (where == TruncateAt.START) {
+ right = len - mt.breakText(0, len, false, avail);
} else if (where == TruncateAt.END) {
- int fit = p.breakText(text, 0, len, true,
- avail - ellipsiswid, null);
-
- if (callback != null) {
- callback.ellipsized(fit, len);
- }
-
- if (preserveLength) {
- return blank(text, fit, len);
- } else {
- return text.toString().substring(0, fit) + sEllipsis;
- }
- } else /* where == TruncateAt.MIDDLE */ {
- int right = p.breakText(text, 0, len, false,
- (avail - ellipsiswid) / 2, null);
- float used = p.measureText(text, len - right, len);
- int left = p.breakText(text, 0, len - right, true,
- avail - ellipsiswid - used, null);
-
- if (callback != null) {
- callback.ellipsized(left, len - right);
- }
-
- if (preserveLength) {
- return blank(text, left, len - right);
- } else {
- String s = text.toString();
- return s.substring(0, left) + sEllipsis +
- s.substring(len - right, len);
- }
+ left = mt.breakText(0, len, true, avail);
+ } else {
+ right = len - mt.breakText(0, len, false, avail / 2);
+ avail -= mt.measure(right, len);
+ left = mt.breakText(0, right, true, avail);
}
- }
-
- // But do the Spanned cases by hand, because it's such a pain
- // to iterate the span transitions backwards and getTextWidths()
- // will give us the information we need.
-
- // getTextWidths() always writes into the start of the array,
- // so measure each span into the first half and then copy the
- // results into the second half to use later.
-
- float[] wid = new float[len * 2];
- TextPaint temppaint = new TextPaint();
- Spanned sp = (Spanned) text;
-
- int next;
- for (int i = 0; i < len; i = next) {
- next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
-
- Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
- System.arraycopy(wid, 0, wid, len + i, next - i);
- }
-
- float sum = 0;
- for (int i = 0; i < len; i++) {
- sum += wid[len + i];
- }
- if (sum <= avail) {
if (callback != null) {
- callback.ellipsized(0, 0);
+ callback.ellipsized(left, right);
}
- return text;
- }
-
- float ellipsiswid = p.measureText(sEllipsis);
-
- if (ellipsiswid > avail) {
- if (callback != null) {
- callback.ellipsized(0, len);
- }
+ char[] buf = mt.mChars;
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+ int remaining = len - (right - left);
if (preserveLength) {
- char[] buf = obtain(len);
- for (int i = 0; i < len; i++) {
+ if (remaining > 0) { // else eliminate the ellipsis too
+ buf[left++] = '\u2026';
+ }
+ for (int i = left; i < right; i++) {
buf[i] = '\uFEFF';
}
- SpannableString ss = new SpannableString(new String(buf, 0, len));
- recycle(buf);
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- return ss;
- } else {
- return "";
- }
- }
-
- if (where == TruncateAt.START) {
- sum = 0;
- int i;
-
- for (i = len; i >= 0; i--) {
- float w = wid[len + i - 1];
-
- if (w + sum + ellipsiswid > avail) {
- break;
+ String s = new String(buf, 0, len);
+ if (sp == null) {
+ return s;
}
-
- sum += w;
- }
-
- if (callback != null) {
- callback.ellipsized(0, i);
- }
-
- if (preserveLength) {
- SpannableString ss = new SpannableString(blank(text, 0, i));
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- return ss;
- } else {
- SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
- out.insert(1, text, i, len);
-
- return out;
- }
- } else if (where == TruncateAt.END) {
- sum = 0;
- int i;
-
- for (i = 0; i < len; i++) {
- float w = wid[len + i];
-
- if (w + sum + ellipsiswid > avail) {
- break;
- }
-
- sum += w;
- }
-
- if (callback != null) {
- callback.ellipsized(i, len);
- }
-
- if (preserveLength) {
- SpannableString ss = new SpannableString(blank(text, i, len));
+ SpannableString ss = new SpannableString(s);
copySpansFrom(sp, 0, len, Object.class, ss, 0);
return ss;
- } else {
- SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
- out.insert(0, text, 0, i);
-
- return out;
- }
- } else /* where = TruncateAt.MIDDLE */ {
- float lsum = 0, rsum = 0;
- int left = 0, right = len;
-
- float ravail = (avail - ellipsiswid) / 2;
- for (right = len; right >= 0; right--) {
- float w = wid[len + right - 1];
-
- if (w + rsum > ravail) {
- break;
- }
-
- rsum += w;
}
- float lavail = avail - ellipsiswid - rsum;
- for (left = 0; left < right; left++) {
- float w = wid[len + left];
-
- if (w + lsum > lavail) {
- break;
- }
-
- lsum += w;
+ if (remaining == 0) {
+ return "";
}
- if (callback != null) {
- callback.ellipsized(left, right);
+ if (sp == null) {
+ StringBuilder sb = new StringBuilder(remaining + sEllipsis.length());
+ sb.append(buf, 0, left);
+ sb.append(sEllipsis);
+ sb.append(buf, right, len - right);
+ return sb.toString();
}
- if (preserveLength) {
- SpannableString ss = new SpannableString(blank(text, left, right));
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- return ss;
- } else {
- SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
- out.insert(0, text, 0, left);
- out.insert(out.length(), text, right, len);
-
- return out;
- }
+ SpannableStringBuilder ssb = new SpannableStringBuilder();
+ ssb.append(text, 0, left);
+ ssb.append(sEllipsis);
+ ssb.append(text, right, len);
+ return ssb;
+ } finally {
+ MeasuredText.recycle(mt);
}
}
- private static String blank(CharSequence source, int start, int end) {
- int len = source.length();
- char[] buf = obtain(len);
-
- if (start != 0) {
- getChars(source, 0, start, buf, 0);
- }
- if (end != len) {
- getChars(source, end, len, buf, end);
- }
-
- if (start != end) {
- buf[start] = '\u2026';
-
- for (int i = start + 1; i < end; i++) {
- buf[i] = '\uFEFF';
- }
- }
-
- String ret = new String(buf, 0, len);
- recycle(buf);
-
- return ret;
- }
-
/**
* Converts a CharSequence of the comma-separated form "Andy, Bob,
* Charles, David" that is too wide to fit into the specified width
@@ -1278,80 +1093,121 @@ public class TextUtils {
TextPaint p, float avail,
String oneMore,
String more) {
- int len = text.length();
- char[] buf = new char[len];
- TextUtils.getChars(text, 0, len, buf, 0);
- int commaCount = 0;
- for (int i = 0; i < len; i++) {
- if (buf[i] == ',') {
- commaCount++;
+ MeasuredText mt = MeasuredText.obtain();
+ try {
+ int len = text.length();
+ float width = setPara(mt, p, text, 0, len, Layout.DIR_REQUEST_DEFAULT_LTR);
+ if (width <= avail) {
+ return text;
}
- }
-
- float[] wid;
- if (text instanceof Spanned) {
- Spanned sp = (Spanned) text;
- TextPaint temppaint = new TextPaint();
- wid = new float[len * 2];
+ char[] buf = mt.mChars;
- int next;
- for (int i = 0; i < len; i = next) {
- next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
-
- Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
- System.arraycopy(wid, 0, wid, len + i, next - i);
+ int commaCount = 0;
+ for (int i = 0; i < len; i++) {
+ if (buf[i] == ',') {
+ commaCount++;
+ }
}
- System.arraycopy(wid, len, wid, 0, len);
- } else {
- wid = new float[len];
- p.getTextWidths(text, 0, len, wid);
- }
+ int remaining = commaCount + 1;
- int ok = 0;
- int okRemaining = commaCount + 1;
- String okFormat = "";
+ int ok = 0;
+ int okRemaining = remaining;
+ String okFormat = "";
- int w = 0;
- int count = 0;
+ int w = 0;
+ int count = 0;
+ float[] widths = mt.mWidths;
- for (int i = 0; i < len; i++) {
- w += wid[i];
+ int request = mt.mDir == 1 ? Layout.DIR_REQUEST_LTR :
+ Layout.DIR_REQUEST_RTL;
- if (buf[i] == ',') {
- count++;
+ MeasuredText tempMt = MeasuredText.obtain();
+ for (int i = 0; i < len; i++) {
+ w += widths[i];
- int remaining = commaCount - count + 1;
- float moreWid;
- String format;
+ if (buf[i] == ',') {
+ count++;
- if (remaining == 1) {
- format = " " + oneMore;
- } else {
- format = " " + String.format(more, remaining);
- }
+ String format;
+ // XXX should not insert spaces, should be part of string
+ // XXX should use plural rules and not assume English plurals
+ if (--remaining == 1) {
+ format = " " + oneMore;
+ } else {
+ format = " " + String.format(more, remaining);
+ }
- moreWid = p.measureText(format);
+ // XXX this is probably ok, but need to look at it more
+ tempMt.setPara(format, 0, format.length(), request);
+ float moreWid = mt.addStyleRun(p, mt.mLen, null);
- if (w + moreWid <= avail) {
- ok = i + 1;
- okRemaining = remaining;
- okFormat = format;
+ if (w + moreWid <= avail) {
+ ok = i + 1;
+ okRemaining = remaining;
+ okFormat = format;
+ }
}
}
- }
+ MeasuredText.recycle(tempMt);
- if (w <= avail) {
- return text;
- } else {
SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
out.insert(0, text, 0, ok);
return out;
+ } finally {
+ MeasuredText.recycle(mt);
}
}
+ private static float setPara(MeasuredText mt, TextPaint paint,
+ CharSequence text, int start, int end, int bidiRequest) {
+
+ mt.setPara(text, start, end, bidiRequest);
+
+ float width;
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+ int len = end - start;
+ if (sp == null) {
+ width = mt.addStyleRun(paint, len, null);
+ } else {
+ width = 0;
+ int spanEnd;
+ for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
+ spanEnd = sp.nextSpanTransition(spanStart, len,
+ MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = sp.getSpans(
+ spanStart, spanEnd, MetricAffectingSpan.class);
+ width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
+ }
+ }
+
+ return width;
+ }
+
+ private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+
+ /* package */
+ static boolean doesNotNeedBidi(CharSequence s, int start, int end) {
+ for (int i = start; i < end; i++) {
+ if (s.charAt(i) >= FIRST_RIGHT_TO_LEFT) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /* package */
+ static boolean doesNotNeedBidi(char[] text, int start, int len) {
+ for (int i = start, e = i + len; i < e; i++) {
+ if (text[i] >= FIRST_RIGHT_TO_LEFT) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/* package */ static char[] obtain(int len) {
char[] buf;
@@ -1529,7 +1385,7 @@ public class TextUtils {
*/
public static final int CAP_MODE_CHARACTERS
= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
-
+
/**
* Capitalization mode for {@link #getCapsMode}: capitalize the first
* character of all words. This value is explicitly defined to be the same as
@@ -1537,7 +1393,7 @@ public class TextUtils {
*/
public static final int CAP_MODE_WORDS
= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
-
+
/**
* Capitalization mode for {@link #getCapsMode}: capitalize the first
* character of each sentence. This value is explicitly defined to be the same as
@@ -1545,13 +1401,13 @@ public class TextUtils {
*/
public static final int CAP_MODE_SENTENCES
= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
-
+
/**
* Determine what caps mode should be in effect at the current offset in
* the text. Only the mode bits set in <var>reqModes</var> will be
* checked. Note that the caps mode flags here are explicitly defined
* to match those in {@link InputType}.
- *
+ *
* @param cs The text that should be checked for caps modes.
* @param off Location in the text at which to check.
* @param reqModes The modes to be checked: may be any combination of
@@ -1651,7 +1507,7 @@ public class TextUtils {
return mode;
}
-
+
private static Object sLock = new Object();
private static char[] sTemp = null;
}
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index 9af42cc..79a0c37 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -16,30 +16,38 @@
package android.text.method;
-import android.util.Log;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
import android.view.KeyEvent;
-import android.graphics.Rect;
-import android.text.*;
-import android.widget.TextView;
-import android.view.View;
-import android.view.ViewConfiguration;
import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.TextView.CursorController;
// XXX this doesn't extend MetaKeyKeyListener because the signatures
// don't match. Need to figure that out. Meanwhile the meta keys
// won't work in fields that don't take input.
-public class
-ArrowKeyMovementMethod
-implements MovementMethod
-{
+public class ArrowKeyMovementMethod implements MovementMethod {
+ /**
+ * An optional controller for the cursor.
+ * Use {@link #setCursorController(CursorController)} to set this field.
+ */
+ protected CursorController mCursorController;
+
+ private boolean isCap(Spannable buffer) {
+ return ((MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_SHIFT_ON) == 1) ||
+ (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
+ }
+
+ private boolean isAlt(Spannable buffer) {
+ return MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_ALT_ON) == 1;
+ }
+
private boolean up(TextView widget, Spannable buffer) {
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
- boolean alt = MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_ALT_ON) == 1;
+ boolean cap = isCap(buffer);
+ boolean alt = isAlt(buffer);
Layout layout = widget.getLayout();
if (cap) {
@@ -60,12 +68,8 @@ implements MovementMethod
}
private boolean down(TextView widget, Spannable buffer) {
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
- boolean alt = MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_ALT_ON) == 1;
+ boolean cap = isCap(buffer);
+ boolean alt = isAlt(buffer);
Layout layout = widget.getLayout();
if (cap) {
@@ -86,12 +90,8 @@ implements MovementMethod
}
private boolean left(TextView widget, Spannable buffer) {
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
- boolean alt = MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_ALT_ON) == 1;
+ boolean cap = isCap(buffer);
+ boolean alt = isAlt(buffer);
Layout layout = widget.getLayout();
if (cap) {
@@ -110,12 +110,8 @@ implements MovementMethod
}
private boolean right(TextView widget, Spannable buffer) {
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
- boolean alt = MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_ALT_ON) == 1;
+ boolean cap = isCap(buffer);
+ boolean alt = isAlt(buffer);
Layout layout = widget.getLayout();
if (cap) {
@@ -133,35 +129,6 @@ implements MovementMethod
}
}
- private int getOffset(int x, int y, TextView widget){
- // Converts the absolute X,Y coordinates to the character offset for the
- // character whose position is closest to the specified
- // horizontal position.
- x -= widget.getTotalPaddingLeft();
- y -= widget.getTotalPaddingTop();
-
- // Clamp the position to inside of the view.
- if (x < 0) {
- x = 0;
- } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) {
- x = widget.getWidth()-widget.getTotalPaddingRight() - 1;
- }
- if (y < 0) {
- y = 0;
- } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) {
- y = widget.getHeight()-widget.getTotalPaddingBottom() - 1;
- }
-
- x += widget.getScrollX();
- y += widget.getScrollY();
-
- Layout layout = widget.getLayout();
- int line = layout.getLineForVertical(y);
-
- int offset = layout.getOffsetForHorizontal(line, x);
- return offset;
- }
-
public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
if (executeDown(widget, buffer, keyCode)) {
MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
@@ -193,10 +160,9 @@ implements MovementMethod
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
- if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
- if (widget.showContextMenu()) {
+ if ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) &&
+ (widget.showContextMenu())) {
handled = true;
- }
}
}
@@ -214,8 +180,7 @@ implements MovementMethod
public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) {
int code = event.getKeyCode();
- if (code != KeyEvent.KEYCODE_UNKNOWN
- && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+ if (code != KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
int repeat = event.getRepeatCount();
boolean handled = false;
while ((--repeat) > 0) {
@@ -226,13 +191,22 @@ implements MovementMethod
return false;
}
- public boolean onTrackballEvent(TextView widget, Spannable text,
- MotionEvent event) {
+ public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
+ if (mCursorController != null) {
+ mCursorController.hide();
+ }
return false;
}
- public boolean onTouchEvent(TextView widget, Spannable buffer,
- MotionEvent event) {
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ if (mCursorController != null) {
+ return onTouchEventCursor(widget, buffer, event);
+ } else {
+ return onTouchEventStandard(widget, buffer, event);
+ }
+ }
+
+ private boolean onTouchEventStandard(TextView widget, Spannable buffer, MotionEvent event) {
int initialScrollX = -1, initialScrollY = -1;
if (event.getAction() == MotionEvent.ACTION_UP) {
initialScrollX = Touch.getInitialScrollX(widget, buffer);
@@ -243,53 +217,20 @@ implements MovementMethod
if (widget.isFocused() && !widget.didTouchFocusSelect()) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
- int x = (int) event.getX();
- int y = (int) event.getY();
- int offset = getOffset(x, y, widget);
-
+ boolean cap = isCap(buffer);
if (cap) {
- buffer.setSpan(LAST_TAP_DOWN, offset, offset,
- Spannable.SPAN_POINT_POINT);
+ int offset = widget.getOffset((int) event.getX(), (int) event.getY());
+
+ buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
// Disallow intercepting of the touch events, so that
// users can scroll and select at the same time.
// without this, users would get booted out of select
// mode once the view detected it needed to scroll.
widget.getParent().requestDisallowInterceptTouchEvent(true);
- } else {
- OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
- OnePointFiveTapState.class);
-
- if (tap.length > 0) {
- if (event.getEventTime() - tap[0].mWhen <=
- ViewConfiguration.getDoubleTapTimeout() &&
- sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) {
-
- tap[0].active = true;
- MetaKeyKeyListener.startSelecting(widget, buffer);
- widget.getParent().requestDisallowInterceptTouchEvent(true);
- buffer.setSpan(LAST_TAP_DOWN, offset, offset,
- Spannable.SPAN_POINT_POINT);
- }
-
- tap[0].mWhen = event.getEventTime();
- } else {
- OnePointFiveTapState newtap = new OnePointFiveTapState();
- newtap.mWhen = event.getEventTime();
- newtap.active = false;
- buffer.setSpan(newtap, 0, buffer.length(),
- Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
+ boolean cap = isCap(buffer);
if (cap && handled) {
// Before selecting, make sure we've moved out of the "slop".
@@ -297,45 +238,15 @@ implements MovementMethod
// OUT of the slop
// Turn long press off while we're selecting. User needs to
- // re-tap on the selection to enable longpress
+ // re-tap on the selection to enable long press
widget.cancelLongPress();
// Update selection as we're moving the selection area.
// Get the current touch position
- int x = (int) event.getX();
- int y = (int) event.getY();
- int offset = getOffset(x, y, widget);
-
- final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
- OnePointFiveTapState.class);
-
- if (tap.length > 0 && tap[0].active) {
- // Get the last down touch position (the position at which the
- // user started the selection)
- int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN);
-
- // Compute the selection boundaries
- int spanstart;
- int spanend;
- if (offset >= lastDownOffset) {
- // Expand from word start of the original tap to new word
- // end, since we are selecting "forwards"
- spanstart = findWordStart(buffer, lastDownOffset);
- spanend = findWordEnd(buffer, offset);
- } else {
- // Expand to from new word start to word end of the original
- // tap since we are selecting "backwards".
- // The spanend will always need to be associated with the touch
- // up position, so that refining the selection with the
- // trackball will work as expected.
- spanstart = findWordEnd(buffer, lastDownOffset);
- spanend = findWordStart(buffer, offset);
- }
- Selection.setSelection(buffer, spanstart, spanend);
- } else {
- Selection.extendSelection(buffer, offset);
- }
+ int offset = widget.getOffset((int) event.getX(), (int) event.getY());
+
+ Selection.extendSelection(buffer, offset);
return true;
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
@@ -344,70 +255,17 @@ implements MovementMethod
// the current scroll offset to avoid the scroll jumping later
// to show it.
if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
- (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
+ (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
widget.moveCursorToVisibleOffset();
return true;
}
- int x = (int) event.getX();
- int y = (int) event.getY();
- int off = getOffset(x, y, widget);
-
- // XXX should do the same adjust for x as we do for the line.
-
- OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(),
- OnePointFiveTapState.class);
- if (onepointfivetap.length > 0 && onepointfivetap[0].active &&
- Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) {
- // If we've set select mode, because there was a onepointfivetap,
- // but there was no ensuing swipe gesture, undo the select mode
- // and remove reference to the last onepointfivetap.
- MetaKeyKeyListener.stopSelecting(widget, buffer);
- for (int i=0; i < onepointfivetap.length; i++) {
- buffer.removeSpan(onepointfivetap[i]);
- }
+ int offset = widget.getOffset((int) event.getX(), (int) event.getY());
+ if (isCap(buffer)) {
buffer.removeSpan(LAST_TAP_DOWN);
- }
- boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
- KeyEvent.META_SHIFT_ON) == 1) ||
- (MetaKeyKeyListener.getMetaState(buffer,
- MetaKeyKeyListener.META_SELECTING) != 0);
-
- DoubleTapState[] tap = buffer.getSpans(0, buffer.length(),
- DoubleTapState.class);
- boolean doubletap = false;
-
- if (tap.length > 0) {
- if (event.getEventTime() - tap[0].mWhen <=
- ViewConfiguration.getDoubleTapTimeout() &&
- sameWord(buffer, off, Selection.getSelectionEnd(buffer))) {
-
- doubletap = true;
- }
-
- tap[0].mWhen = event.getEventTime();
- } else {
- DoubleTapState newtap = new DoubleTapState();
- newtap.mWhen = event.getEventTime();
- buffer.setSpan(newtap, 0, buffer.length(),
- Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
-
- if (cap) {
- buffer.removeSpan(LAST_TAP_DOWN);
- if (onepointfivetap.length > 0 && onepointfivetap[0].active) {
- // If we selecting something with the onepointfivetap-and
- // swipe gesture, stop it on finger up.
- MetaKeyKeyListener.stopSelecting(widget, buffer);
- } else {
- Selection.extendSelection(buffer, off);
- }
- } else if (doubletap) {
- Selection.setSelection(buffer,
- findWordStart(buffer, off),
- findWordEnd(buffer, off));
+ Selection.extendSelection(buffer, offset);
} else {
- Selection.setSelection(buffer, off);
+ Selection.setSelection(buffer, offset);
}
MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
@@ -420,73 +278,36 @@ implements MovementMethod
return handled;
}
- private static class DoubleTapState implements NoCopySpan {
- long mWhen;
- }
-
- /* We check for a onepointfive tap. This is similar to
- * doubletap gesture (where a finger goes down, up, down, up, in a short
- * time period), except in the onepointfive tap, a users finger only needs
- * to go down, up, down in a short time period. We detect this type of tap
- * to implement the onepointfivetap-and-swipe selection gesture.
- * This gesture allows users to select a segment of text without going
- * through the "select text" option in the context menu.
- */
- private static class OnePointFiveTapState implements NoCopySpan {
- long mWhen;
- boolean active;
- }
-
- private static boolean sameWord(CharSequence text, int one, int two) {
- int start = findWordStart(text, one);
- int end = findWordEnd(text, one);
-
- if (end == start) {
- return false;
- }
+ private boolean onTouchEventCursor(TextView widget, Spannable buffer, MotionEvent event) {
+ if (widget.isFocused() && !widget.didTouchFocusSelect()) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ widget.cancelLongPress();
- return start == findWordStart(text, two) &&
- end == findWordEnd(text, two);
- }
+ // Offset the current touch position (from controller to cursor)
+ final float x = event.getX() + mCursorController.getOffsetX();
+ final float y = event.getY() + mCursorController.getOffsetY();
+ int offset = widget.getOffset((int) x, (int) y);
+ mCursorController.updatePosition(offset);
+ return true;
- // TODO: Unify with TextView.getWordForDictionary()
- private static int findWordStart(CharSequence text, int start) {
- for (; start > 0; start--) {
- char c = text.charAt(start - 1);
- int type = Character.getType(c);
-
- if (c != '\'' &&
- type != Character.UPPERCASE_LETTER &&
- type != Character.LOWERCASE_LETTER &&
- type != Character.TITLECASE_LETTER &&
- type != Character.MODIFIER_LETTER &&
- type != Character.DECIMAL_DIGIT_NUMBER) {
- break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mCursorController = null;
+ return true;
}
}
-
- return start;
+ return false;
}
- // TODO: Unify with TextView.getWordForDictionary()
- private static int findWordEnd(CharSequence text, int end) {
- int len = text.length();
-
- for (; end < len; end++) {
- char c = text.charAt(end);
- int type = Character.getType(c);
-
- if (c != '\'' &&
- type != Character.UPPERCASE_LETTER &&
- type != Character.LOWERCASE_LETTER &&
- type != Character.TITLECASE_LETTER &&
- type != Character.MODIFIER_LETTER &&
- type != Character.DECIMAL_DIGIT_NUMBER) {
- break;
- }
- }
-
- return end;
+ /**
+ * Defines the cursor controller.
+ *
+ * When set, this object can be used to handle events, that can be translated in cursor updates.
+ * @param cursorController A cursor controller implementation
+ */
+ public void setCursorController(CursorController cursorController) {
+ mCursorController = cursorController;
}
public boolean canSelectArbitrarily() {
@@ -525,8 +346,9 @@ implements MovementMethod
}
public static MovementMethod getInstance() {
- if (sInstance == null)
+ if (sInstance == null) {
sInstance = new ArrowKeyMovementMethod();
+ }
return sInstance;
}
diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java
index 42ad10e..3b98fc3 100644
--- a/core/java/android/text/method/Touch.java
+++ b/core/java/android/text/method/Touch.java
@@ -17,14 +17,13 @@
package android.text.method;
import android.text.Layout;
-import android.text.NoCopySpan;
import android.text.Layout.Alignment;
+import android.text.NoCopySpan;
import android.text.Spannable;
-import android.util.Log;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.TextView;
-import android.view.KeyEvent;
public class Touch {
private Touch() { }
@@ -45,6 +44,7 @@ public class Touch {
int left = Integer.MAX_VALUE;
int right = 0;
Alignment a = null;
+ boolean ltr = true;
for (int i = top; i <= bottom; i++) {
left = (int) Math.min(left, layout.getLineLeft(i));
@@ -52,6 +52,7 @@ public class Touch {
if (a == null) {
a = layout.getParagraphAlignment(i);
+ ltr = layout.getParagraphDirection(i) > 0;
}
}
@@ -59,10 +60,12 @@ public class Touch {
int width = widget.getWidth();
int diff = 0;
+ // align_opposite does NOT mean align_right, we need the paragraph
+ // direction to resolve it to left or right
if (right - left < width - padding) {
if (a == Alignment.ALIGN_CENTER) {
diff = (width - padding - (right - left)) / 2;
- } else if (a == Alignment.ALIGN_OPPOSITE) {
+ } else if (ltr == (a == Alignment.ALIGN_OPPOSITE)) {
diff = width - padding - (right - left);
}
}
@@ -99,7 +102,7 @@ public class Touch {
MotionEvent event) {
DragState[] ds;
- switch (event.getAction()) {
+ switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
ds = buffer.getSpans(0, buffer.length(), DragState.class);
diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java
index 9d91aca..a763a69 100644
--- a/core/java/android/util/CharsetUtils.java
+++ b/core/java/android/util/CharsetUtils.java
@@ -17,36 +17,58 @@
package android.util;
import android.os.Build;
+import android.text.TextUtils;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
+import java.util.HashMap;
+import java.util.Map;
/**
+ * <p>
* A class containing utility methods related to character sets. This
* class is primarily useful for code that wishes to be vendor-aware
- * in its interpretation of Japanese encoding names.
- *
- * <p>As of this writing, the only vendor that is recognized by this
- * class is Docomo (identified case-insensitively as {@code "docomo"}).</p>
- *
- * <b>Note:</b> This class is hidden in Cupcake, with a plan to
- * un-hide in Donut. This was done because the first deployment to use
- * this code is based on Cupcake, but the API had to be introduced
- * after the public API freeze for that release. The upshot is that
- * only system applications can safely use this class until Donut is
- * available.
- *
+ * in its interpretation of Japanese charset names (used in DoCoMo,
+ * KDDI, and SoftBank).
+ * </p>
+ *
+ * <p>
+ * <b>Note:</b> Developers will need to add an appropriate mapping for
+ * each vendor-specific charset. You may need to modify the C libraries
+ * like icu4c in order to let Android support an additional charset.
+ * </p>
+ *
* @hide
*/
public final class CharsetUtils {
/**
- * name of the vendor "Docomo". <b>Note:</b> This isn't a public
+ * name of the vendor "DoCoMo". <b>Note:</b> This isn't a public
* constant, in order to keep this class from becoming a de facto
* reference list of vendor names.
*/
private static final String VENDOR_DOCOMO = "docomo";
-
+ /**
+ * Name of the vendor "KDDI".
+ */
+ private static final String VENDOR_KDDI = "kddi";
+ /**
+ * Name of the vendor "SoftBank".
+ */
+ private static final String VENDOR_SOFTBANK = "softbank";
+
+ /**
+ * Represents one-to-one mapping from a vendor name to a charset specific to the vendor.
+ */
+ private static final Map<String, String> sVendorShiftJisMap = new HashMap<String, String>();
+
+ static {
+ // These variants of Shift_JIS come from icu's mapping data (convrtrs.txt)
+ sVendorShiftJisMap.put(VENDOR_DOCOMO, "docomo-shift_jis-2007");
+ sVendorShiftJisMap.put(VENDOR_KDDI, "kddi-shift_jis-2007");
+ sVendorShiftJisMap.put(VENDOR_SOFTBANK, "softbank-shift_jis-2007");
+ }
+
/**
* This class is uninstantiable.
*/
@@ -58,20 +80,22 @@ public final class CharsetUtils {
* Returns the name of the vendor-specific character set
* corresponding to the given original character set name and
* vendor. If there is no vendor-specific character set for the
- * given name/vendor pair, this returns the original character set
- * name. The vendor name is matched case-insensitively.
- *
+ * given name/vendor pair, this returns the original character set name.
+ *
* @param charsetName the base character set name
- * @param vendor the vendor to specialize for
+ * @param vendor the vendor to specialize for. All characters should be lower-cased.
* @return the specialized character set name, or {@code charsetName} if
* there is no specialized name
*/
public static String nameForVendor(String charsetName, String vendor) {
- // TODO: Eventually, this may want to be table-driven.
-
- if (vendor.equalsIgnoreCase(VENDOR_DOCOMO)
- && isShiftJis(charsetName)) {
- return "docomo-shift_jis-2007";
+ if (!TextUtils.isEmpty(charsetName) && !TextUtils.isEmpty(vendor)) {
+ // You can add your own mapping here.
+ if (isShiftJis(charsetName)) {
+ final String vendorShiftJis = sVendorShiftJisMap.get(vendor);
+ if (vendorShiftJis != null) {
+ return vendorShiftJis;
+ }
+ }
}
return charsetName;
diff --git a/core/java/android/util/Patterns.java b/core/java/android/util/Patterns.java
index 5cbfd29..3bcd266 100644
--- a/core/java/android/util/Patterns.java
+++ b/core/java/android/util/Patterns.java
@@ -25,7 +25,7 @@ import java.util.regex.Pattern;
public class Patterns {
/**
* Regular expression to match all IANA top-level domains.
- * List accurate as of 2010/02/05. List taken from:
+ * List accurate as of 2010/05/06. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py
*/
@@ -53,8 +53,8 @@ public class Patterns {
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
- + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)"
- + "|y[etu]"
+ + "|(xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)"
+ + "|y[et]"
+ "|z[amw])";
/**
@@ -65,7 +65,7 @@ public class Patterns {
/**
* Regular expression to match all IANA top-level domains for WEB_URL.
- * List accurate as of 2010/02/05. List taken from:
+ * List accurate as of 2010/05/06. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py
*/
@@ -94,8 +94,8 @@ public class Patterns {
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
- + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)"
- + "|y[etu]"
+ + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-mgbaam7a8h|xn\\-\\-mgberp4a5d4ar|xn\\-\\-wgbh1c|xn\\-\\-zckzah)"
+ + "|y[et]"
+ "|z[amw]))";
/**
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index 1c8b330..7fc43b9 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -90,6 +90,16 @@ public class SparseArray<E> {
delete(key);
}
+ /**
+ * Removes the mapping at the specified index.
+ */
+ public void removeAt(int index) {
+ if (mValues[index] != DELETED) {
+ mValues[index] = DELETED;
+ mGarbage = true;
+ }
+ }
+
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
diff --git a/core/java/android/view/AbsSavedState.java b/core/java/android/view/AbsSavedState.java
index 840d7c1..6ad33dd 100644
--- a/core/java/android/view/AbsSavedState.java
+++ b/core/java/android/view/AbsSavedState.java
@@ -54,7 +54,7 @@ public abstract class AbsSavedState implements Parcelable {
*/
protected AbsSavedState(Parcel source) {
// FIXME need class loader
- Parcelable superState = (Parcelable) source.readParcelable(null);
+ Parcelable superState = source.readParcelable(null);
mSuperState = superState != null ? superState : EMPTY_STATE;
}
@@ -75,7 +75,7 @@ public abstract class AbsSavedState implements Parcelable {
= new Parcelable.Creator<AbsSavedState>() {
public AbsSavedState createFromParcel(Parcel in) {
- Parcelable superState = (Parcelable) in.readParcelable(null);
+ Parcelable superState = in.readParcelable(null);
if (superState != null) {
throw new IllegalStateException("superState must be null");
}
diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java
new file mode 100644
index 0000000..0ad3c0b
--- /dev/null
+++ b/core/java/android/view/GLES20Canvas.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.DrawFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Picture;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+
+import javax.microedition.khronos.opengles.GL;
+
+/**
+ * An implementation of Canvas on top of OpenGL ES 2.0.
+ */
+@SuppressWarnings({"deprecation"})
+class GLES20Canvas extends Canvas {
+ @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
+ private final GL mGl;
+ private final boolean mOpaque;
+ private final int mRenderer;
+
+ private int mWidth;
+ private int mHeight;
+
+ private final float[] mPoint = new float[2];
+ private final float[] mLine = new float[4];
+
+ private final Rect mClipBounds = new Rect();
+
+ private DrawFilter mFilter;
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructors
+ ///////////////////////////////////////////////////////////////////////////
+
+ GLES20Canvas(GL gl, boolean translucent) {
+ mGl = gl;
+ mOpaque = !translucent;
+
+ mRenderer = nCreateRenderer();
+ }
+
+ private native int nCreateRenderer();
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ super.finalize();
+ } finally {
+ nDestroyRenderer(mRenderer);
+ }
+ }
+
+ private native void nDestroyRenderer(int renderer);
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Canvas management
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public boolean isHardwareAccelerated() {
+ return true;
+ }
+
+ @Override
+ public GL getGL() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setBitmap(Bitmap bitmap) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Setup
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void setViewport(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+
+ nSetViewport(mRenderer, width, height);
+ }
+
+ private native void nSetViewport(int renderer, int width, int height);
+
+ void onPreDraw() {
+ nPrepare(mRenderer);
+ }
+
+ private native void nPrepare(int renderer);
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Clipping
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public boolean clipPath(Path path) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean clipPath(Path path, Region.Op op) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean clipRect(float left, float top, float right, float bottom) {
+ return nClipRect(mRenderer, left, top, right, bottom);
+ }
+
+ private native boolean nClipRect(int renderer, float left, float top, float right, float bottom);
+
+ @Override
+ public boolean clipRect(float left, float top, float right, float bottom, Region.Op op) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean clipRect(int left, int top, int right, int bottom) {
+ return nClipRect(mRenderer, left, top, right, bottom);
+ }
+
+ private native boolean nClipRect(int renderer, int left, int top, int right, int bottom);
+
+ @Override
+ public boolean clipRect(Rect rect) {
+ return clipRect(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ @Override
+ public boolean clipRect(Rect rect, Region.Op op) {
+ // TODO: Implement
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean clipRect(RectF rect) {
+ return clipRect(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ @Override
+ public boolean clipRect(RectF rect, Region.Op op) {
+ // TODO: Implement
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean clipRegion(Region region) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean clipRegion(Region region, Region.Op op) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getClipBounds(Rect bounds) {
+ return nGetClipBounds(mRenderer, bounds);
+ }
+
+ private native boolean nGetClipBounds(int renderer, Rect bounds);
+
+ @Override
+ public boolean quickReject(float left, float top, float right, float bottom, EdgeType type) {
+ return nQuickReject(mRenderer, left, top, right, bottom, type.nativeInt);
+ }
+
+ private native boolean nQuickReject(int renderer, float left, float top,
+ float right, float bottom, int edge);
+
+ @Override
+ public boolean quickReject(Path path, EdgeType type) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean quickReject(RectF rect, EdgeType type) {
+ return quickReject(rect.left, rect.top, rect.right, rect.bottom, type);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Transformations
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void translate(float dx, float dy) {
+ nTranslate(mRenderer, dx, dy);
+ }
+
+ private native void nTranslate(int renderer, float dx, float dy);
+
+ @Override
+ public void skew(float sx, float sy) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void rotate(float degrees) {
+ nRotate(mRenderer, degrees);
+ }
+
+ private native void nRotate(int renderer, float degrees);
+
+ @Override
+ public void scale(float sx, float sy) {
+ nScale(mRenderer, sx, sy);
+ }
+
+ private native void nScale(int renderer, float sx, float sy);
+
+ @Override
+ public void setMatrix(Matrix matrix) {
+ nSetMatrix(mRenderer, matrix.native_instance);
+ }
+
+ private native void nSetMatrix(int renderer, int matrix);
+
+ @Override
+ public void getMatrix(Matrix matrix) {
+ nGetMatrix(mRenderer, matrix.native_instance);
+ }
+
+ private native void nGetMatrix(int renderer, int matrix);
+
+ @Override
+ public void concat(Matrix matrix) {
+ nConcatMatrix(mRenderer, matrix.native_instance);
+ }
+
+ private native void nConcatMatrix(int renderer, int matrix);
+
+ ///////////////////////////////////////////////////////////////////////////
+ // State management
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public int save() {
+ return nSave(mRenderer, 0);
+ }
+
+ @Override
+ public int save(int saveFlags) {
+ return nSave(mRenderer, saveFlags);
+ }
+
+ private native int nSave(int renderer, int flags);
+
+ @Override
+ public int saveLayer(RectF bounds, Paint paint, int saveFlags) {
+ return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags);
+ }
+
+ @Override
+ public int saveLayer(float left, float top, float right, float bottom, Paint paint,
+ int saveFlags) {
+ int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ return nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags);
+ }
+
+ private native int nSaveLayer(int renderer, float left, float top, float right, float bottom,
+ int paint, int saveFlags);
+
+ @Override
+ public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags) {
+ return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom,
+ alpha, saveFlags);
+ }
+
+ @Override
+ public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
+ int saveFlags) {
+ return nSaveLayerAlpha(mRenderer, left, top, right, bottom, alpha, saveFlags);
+ }
+
+ private native int nSaveLayerAlpha(int renderer, float left, float top, float right,
+ float bottom, int alpha, int saveFlags);
+
+ @Override
+ public void restore() {
+ nRestore(mRenderer);
+ }
+
+ private native void nRestore(int renderer);
+
+ @Override
+ public void restoreToCount(int saveCount) {
+ nRestoreToCount(mRenderer, saveCount);
+ }
+
+ private native void nRestoreToCount(int renderer, int saveCount);
+
+ @Override
+ public int getSaveCount() {
+ return nGetSaveCount(mRenderer);
+ }
+
+ private native int nGetSaveCount(int renderer);
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Filtering
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void setDrawFilter(DrawFilter filter) {
+ // Don't crash, but ignore the draw filter
+ // TODO: Implement PaintDrawFilter
+ mFilter = filter;
+ }
+
+ @Override
+ public DrawFilter getDrawFilter() {
+ return mFilter;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Drawing
+ ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,
+ Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawARGB(int a, int r, int g, int b) {
+ drawColor((a & 0xFF) << 24 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF));
+ }
+
+ @Override
+ public void drawPatch(Bitmap bitmap, byte[] chunks, RectF dst, Paint paint) {
+ final int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ nDrawPatch(mRenderer, bitmap.mNativeBitmap, chunks, dst.left, dst.top,
+ dst.right, dst.bottom, nativePaint);
+ }
+
+ private native void nDrawPatch(int renderer, int bitmap, byte[] chunks, float left, float top,
+ float right, float bottom, int paint);
+
+ @Override
+ public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
+ final int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ nDrawBitmap(mRenderer, bitmap.mNativeBitmap, left, top, nativePaint);
+ }
+
+ private native void nDrawBitmap(int renderer, int bitmap, float left, float top, int paint);
+
+ @Override
+ public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {
+ final int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ nDrawBitmap(mRenderer, bitmap.mNativeBitmap, matrix.native_instance, nativePaint);
+ }
+
+ private native void nDrawBitmap(int renderer, int bitmap, int matrix, int paint);
+
+ @Override
+ public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) {
+ final int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ nDrawBitmap(mRenderer, bitmap.mNativeBitmap, src.left, src.top, src.right, src.bottom,
+ dst.left, dst.top, dst.right, dst.bottom, nativePaint
+ );
+ }
+
+ @Override
+ public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
+ final int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ nDrawBitmap(mRenderer, bitmap.mNativeBitmap, src.left, src.top, src.right, src.bottom,
+ dst.left, dst.top, dst.right, dst.bottom, nativePaint
+ );
+ }
+
+ private native void nDrawBitmap(int renderer, int bitmap,
+ float srcLeft, float srcTop, float srcRight, float srcBottom,
+ float left, float top, float right, float bottom, int paint);
+
+ @Override
+ public void drawBitmap(int[] colors, int offset, int stride, float x, float y,
+ int width, int height, boolean hasAlpha, Paint paint) {
+ final Bitmap.Config config = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
+ final Bitmap b = Bitmap.createBitmap(colors, offset, stride, width, height, config);
+ final int nativePaint = paint == null ? 0 : paint.mNativePaint;
+ nDrawBitmap(mRenderer, b.mNativeBitmap, x, y, nativePaint);
+ b.recycle();
+ }
+
+ @Override
+ public void drawBitmap(int[] colors, int offset, int stride, int x, int y,
+ int width, int height, boolean hasAlpha, Paint paint) {
+ drawBitmap(colors, offset, stride, (float) x, (float) y, width, height, hasAlpha, paint);
+ }
+
+ @Override
+ public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts,
+ int vertOffset, int[] colors, int colorOffset, Paint paint) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void drawCircle(float cx, float cy, float radius, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawColor(int color) {
+ drawColor(color, PorterDuff.Mode.SRC_OVER);
+ }
+
+ @Override
+ public void drawColor(int color, PorterDuff.Mode mode) {
+ nDrawColor(mRenderer, color, mode.nativeInt);
+ }
+
+ private native void nDrawColor(int renderer, int color, int mode);
+
+ @Override
+ public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
+ mLine[0] = startX;
+ mLine[1] = startY;
+ mLine[2] = stopX;
+ mLine[3] = stopY;
+ drawLines(mLine, 0, 1, paint);
+ }
+
+ @Override
+ public void drawLines(float[] pts, int offset, int count, Paint paint) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void drawLines(float[] pts, Paint paint) {
+ drawLines(pts, 0, pts.length / 4, paint);
+ }
+
+ @Override
+ public void drawOval(RectF oval, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawPaint(Paint paint) {
+ final Rect r = mClipBounds;
+ nGetClipBounds(mRenderer, r);
+ drawRect(r.left, r.top, r.right, r.bottom, paint);
+ }
+
+ @Override
+ public void drawPath(Path path, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawPicture(Picture picture) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawPicture(Picture picture, Rect dst) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawPicture(Picture picture, RectF dst) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawPoint(float x, float y, Paint paint) {
+ mPoint[0] = x;
+ mPoint[1] = y;
+ drawPoints(mPoint, 0, 1, paint);
+ }
+
+ @Override
+ public void drawPoints(float[] pts, int offset, int count, Paint paint) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void drawPoints(float[] pts, Paint paint) {
+ drawPoints(pts, 0, pts.length / 2, paint);
+ }
+
+ @Override
+ public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawPosText(String text, float[] pos, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawRect(float left, float top, float right, float bottom, Paint paint) {
+ nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint);
+ }
+
+ private native void nDrawRect(int renderer, float left, float top, float right, float bottom,
+ int paint);
+
+ @Override
+ public void drawRect(Rect r, Paint paint) {
+ drawRect(r.left, r.top, r.right, r.bottom, paint);
+ }
+
+ @Override
+ public void drawRect(RectF r, Paint paint) {
+ drawRect(r.left, r.top, r.right, r.bottom, paint);
+ }
+
+ @Override
+ public void drawRGB(int r, int g, int b) {
+ drawColor(0xFF000000 | (r & 0xFF) << 16 | (g & 0xFF) << 8 | (b & 0xFF));
+ }
+
+ @Override
+ public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void drawText(String text, int start, int end, float x, float y, Paint paint) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void drawText(String text, float x, float y, Paint paint) {
+ drawText(text, 0, text.length(), x, y, paint);
+ }
+
+ @Override
+ public void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset,
+ float vOffset, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount,
+ float x, float y, int dir, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd,
+ float x, float y, int dir, Paint paint) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset,
+ float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices,
+ int indexOffset, int indexCount, Paint paint) {
+ // TODO: Implement
+ }
+}
diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
new file mode 100644
index 0000000..090a743
--- /dev/null
+++ b/core/java/android/view/HardwareRenderer.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.view;
+
+import android.graphics.Canvas;
+import android.os.SystemClock;
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGL11;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL11;
+
+import static javax.microedition.khronos.opengles.GL10.GL_COLOR_BUFFER_BIT;
+import static javax.microedition.khronos.opengles.GL10.GL_SCISSOR_TEST;
+
+/**
+ * Interface for rendering a ViewRoot using hardware acceleration.
+ *
+ * @hide
+ */
+abstract class HardwareRenderer {
+ private boolean mEnabled;
+ private boolean mRequested = true;
+ private static final String LOG_TAG = "HardwareRenderer";
+
+ /**
+ * Destroys the hardware rendering context.
+ */
+ abstract void destroy();
+
+ /**
+ * Initializes the hardware renderer for the specified surface.
+ *
+ * @param holder The holder for the surface to hardware accelerate.
+ *
+ * @return True if the initialization was successful, false otherwise.
+ */
+ abstract boolean initialize(SurfaceHolder holder);
+
+ /**
+ * Setup the hardware renderer for drawing. This is called for every
+ * frame to draw.
+ *
+ * @param width Width of the drawing surface.
+ * @param height Height of the drawing surface.
+ * @param attachInfo The AttachInfo used to render the ViewRoot.
+ */
+ abstract void setup(int width, int height, View.AttachInfo attachInfo);
+
+ /**
+ * Draws the specified view.
+ *
+ * @param view The view to draw.
+ * @param attachInfo AttachInfo tied to the specified view.
+ */
+ abstract void draw(View view, View.AttachInfo attachInfo, int yOffset);
+
+ /**
+ * Initializes the hardware renderer for the specified surface and setup the
+ * renderer for drawing, if needed. This is invoked when the ViewRoot has
+ * potentially lost the hardware renderer. The hardware renderer should be
+ * reinitialized and setup when the render {@link #isRequested()} and
+ * {@link #isEnabled()}.
+ *
+ * @param width The width of the drawing surface.
+ * @param height The height of the drawing surface.
+ * @param attachInfo The
+ * @param holder
+ */
+ void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo,
+ SurfaceHolder holder) {
+
+ if (isRequested()) {
+ // We lost the gl context, so recreate it.
+ if (!isEnabled()) {
+ if (initialize(holder)) {
+ setup(width, height, attachInfo);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a hardware renderer using OpenGL.
+ *
+ * @param glVersion The version of OpenGL to use (1 for OpenGL 1, 11 for OpenGL 1.1, etc.)
+ * @param translucent True if the surface is translucent, false otherwise
+ *
+ * @return A hardware renderer backed by OpenGL.
+ */
+ static HardwareRenderer createGlRenderer(int glVersion, boolean translucent) {
+ switch (glVersion) {
+ case 1:
+ return new Gl10Renderer(translucent);
+ case 2:
+ return new Gl20Renderer(translucent);
+ }
+ throw new IllegalArgumentException("Unknown GL version: " + glVersion);
+ }
+
+ /**
+ * Indicates whether hardware acceleration is currently enabled.
+ *
+ * @return True if hardware acceleration is in use, false otherwise.
+ */
+ boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Indicates whether hardware acceleration is currently enabled.
+ *
+ * @param enabled True if the hardware renderer is in use, false otherwise.
+ */
+ void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Indicates whether hardware acceleration is currently request but not
+ * necessarily enabled yet.
+ *
+ * @return True if requested, false otherwise.
+ */
+ boolean isRequested() {
+ return mRequested;
+ }
+
+ /**
+ * Indicates whether hardware acceleration is currently request but not
+ * necessarily enabled yet.
+ *
+ * @return True to request hardware acceleration, false otherwise.
+ */
+ void setRequested(boolean requested) {
+ mRequested = requested;
+ }
+
+ @SuppressWarnings({"deprecation"})
+ static abstract class GlRenderer extends HardwareRenderer {
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+
+ EGL10 mEgl;
+ EGLDisplay mEglDisplay;
+ EGLContext mEglContext;
+ EGLSurface mEglSurface;
+ EGLConfig mEglConfig;
+
+ GL mGl;
+ Canvas mCanvas;
+
+ final int mGlVersion;
+ final boolean mTranslucent;
+
+ GlRenderer(int glVersion, boolean translucent) {
+ mGlVersion = glVersion;
+ mTranslucent = translucent;
+ }
+
+ /**
+ * Checks for OpenGL errors. If an error has occured, {@link #destroy()}
+ * is invoked and the requested flag is turned off. The error code is
+ * also logged as a warning.
+ */
+ void checkErrors() {
+ if (isEnabled()) {
+ int error = mEgl.eglGetError();
+ if (error != EGL10.EGL_SUCCESS) {
+ // something bad has happened revert to
+ // normal rendering.
+ destroy();
+ if (error != EGL11.EGL_CONTEXT_LOST) {
+ // we'll try again if it was context lost
+ setRequested(false);
+ }
+ Log.w(LOG_TAG, "OpenGL error: " + error);
+ }
+ }
+ }
+
+ @Override
+ boolean initialize(SurfaceHolder holder) {
+ if (isRequested() && !isEnabled()) {
+ initializeEgl();
+ mGl = createEglSurface(holder);
+
+ if (mGl != null) {
+ int err = mEgl.eglGetError();
+ if (err != EGL10.EGL_SUCCESS) {
+ destroy();
+ setRequested(false);
+ } else {
+ mCanvas = createCanvas();
+ if (mCanvas != null) {
+ setEnabled(true);
+ } else {
+ Log.w(LOG_TAG, "Hardware accelerated Canvas could not be created");
+ }
+ }
+
+ return mCanvas != null;
+ }
+ }
+ return false;
+ }
+
+ abstract Canvas createCanvas();
+
+ void initializeEgl() {
+ mEgl = (EGL10) EGLContext.getEGL();
+
+ // Get to the default display.
+ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+ throw new RuntimeException("eglGetDisplay failed");
+ }
+
+ // We can now initialize EGL for that display
+ int[] version = new int[2];
+ if (!mEgl.eglInitialize(mEglDisplay, version)) {
+ throw new RuntimeException("eglInitialize failed");
+ }
+ mEglConfig = getConfigChooser(mGlVersion).chooseConfig(mEgl, mEglDisplay);
+
+ /*
+ * Create an EGL context. We want to do this as rarely as we can, because an
+ * EGL context is a somewhat heavy object.
+ */
+ mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
+ }
+
+ GL createEglSurface(SurfaceHolder holder) {
+ // Check preconditions.
+ if (mEgl == null) {
+ throw new RuntimeException("egl not initialized");
+ }
+ if (mEglDisplay == null) {
+ throw new RuntimeException("eglDisplay not initialized");
+ }
+ if (mEglConfig == null) {
+ throw new RuntimeException("mEglConfig not initialized");
+ }
+
+ /*
+ * The window size has changed, so we need to create a new
+ * surface.
+ */
+ if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+
+ /*
+ * Unbind and destroy the old EGL surface, if
+ * there is one.
+ */
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+ mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+ }
+
+ // Create an EGL surface we can render into.
+ mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, holder, null);
+
+ if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+ int error = mEgl.eglGetError();
+ if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
+ Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+ return null;
+ }
+ throw new RuntimeException("createWindowSurface failed");
+ }
+
+ /*
+ * Before we can issue GL commands, we need to make sure
+ * the context is current and bound to a surface.
+ */
+ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+ throw new RuntimeException("eglMakeCurrent failed");
+
+ }
+
+ return mEglContext.getGL();
+ }
+
+ EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
+ int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, mGlVersion, EGL10.EGL_NONE };
+
+ return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT,
+ mGlVersion != 0 ? attrib_list : null);
+ }
+
+ @Override
+ void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo,
+ SurfaceHolder holder) {
+
+ if (isRequested()) {
+ checkErrors();
+ super.initializeIfNeeded(width, height, attachInfo, holder);
+ }
+ }
+
+ @Override
+ void destroy() {
+ if (!isEnabled()) return;
+
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+ mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+ mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+ mEgl.eglTerminate(mEglDisplay);
+
+ mEglContext = null;
+ mEglSurface = null;
+ mEglDisplay = null;
+ mEgl = null;
+ mGl = null;
+ mCanvas = null;
+
+ setEnabled(false);
+ }
+
+ @Override
+ void setup(int width, int height, View.AttachInfo attachInfo) {
+ final float scale = attachInfo.mApplicationScale;
+ mCanvas.setViewport((int) (width * scale + 0.5f), (int) (height * scale + 0.5f));
+ }
+
+ boolean canDraw() {
+ return mGl != null && mCanvas != null;
+ }
+
+ void onPreDraw() {
+ }
+
+ /**
+ * Defines the EGL configuration for this renderer. The default configuration
+ * is RGBX, no depth, no stencil.
+ *
+ * @return An {@link android.view.HardwareRenderer.GlRenderer.EglConfigChooser}.
+ * @param glVersion
+ */
+ EglConfigChooser getConfigChooser(int glVersion) {
+ return new ComponentSizeChooser(glVersion, 8, 8, 8, mTranslucent ? 8 : 0, 0, 0);
+ }
+
+ @Override
+ void draw(View view, View.AttachInfo attachInfo, int yOffset) {
+ if (canDraw()) {
+ attachInfo.mDrawingTime = SystemClock.uptimeMillis();
+ attachInfo.mIgnoreDirtyState = true;
+ view.mPrivateFlags |= View.DRAWN;
+
+ onPreDraw();
+
+ Canvas canvas = mCanvas;
+ int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+ canvas.translate(0, -yOffset);
+
+ try {
+ view.draw(canvas);
+ } finally {
+ canvas.restoreToCount(saveCount);
+ }
+
+ attachInfo.mIgnoreDirtyState = false;
+
+ mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+ checkErrors();
+ }
+ }
+
+ static abstract class EglConfigChooser {
+ final int[] mConfigSpec;
+ private final int mGlVersion;
+
+ EglConfigChooser(int glVersion, int[] configSpec) {
+ mGlVersion = glVersion;
+ mConfigSpec = filterConfigSpec(configSpec);
+ }
+
+ EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+ int[] index = new int[1];
+ if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, index)) {
+ throw new IllegalArgumentException("eglChooseConfig failed");
+ }
+
+ int numConfigs = index[0];
+ if (numConfigs <= 0) {
+ throw new IllegalArgumentException("No configs match configSpec");
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfigs];
+ if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, index)) {
+ throw new IllegalArgumentException("eglChooseConfig failed");
+ }
+
+ EGLConfig config = chooseConfig(egl, display, configs);
+ if (config == null) {
+ throw new IllegalArgumentException("No config chosen");
+ }
+
+ return config;
+ }
+
+ abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs);
+
+ private int[] filterConfigSpec(int[] configSpec) {
+ if (mGlVersion != 2) {
+ return configSpec;
+ }
+ /* We know none of the subclasses define EGL_RENDERABLE_TYPE.
+ * And we know the configSpec is well formed.
+ */
+ int len = configSpec.length;
+ int[] newConfigSpec = new int[len + 2];
+ System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1);
+ newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE;
+ newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */
+ newConfigSpec[len + 1] = EGL10.EGL_NONE;
+ return newConfigSpec;
+ }
+ }
+
+ /**
+ * Choose a configuration with exactly the specified r,g,b,a sizes,
+ * and at least the specified depth and stencil sizes.
+ */
+ static class ComponentSizeChooser extends EglConfigChooser {
+ private int[] mValue;
+
+ private int mRedSize;
+ private int mGreenSize;
+ private int mBlueSize;
+ private int mAlphaSize;
+ private int mDepthSize;
+ private int mStencilSize;
+
+ ComponentSizeChooser(int glVersion, int redSize, int greenSize, int blueSize,
+ int alphaSize, int depthSize, int stencilSize) {
+ super(glVersion, new int[] {
+ EGL10.EGL_RED_SIZE, redSize,
+ EGL10.EGL_GREEN_SIZE, greenSize,
+ EGL10.EGL_BLUE_SIZE, blueSize,
+ EGL10.EGL_ALPHA_SIZE, alphaSize,
+ EGL10.EGL_DEPTH_SIZE, depthSize,
+ EGL10.EGL_STENCIL_SIZE, stencilSize,
+ EGL10.EGL_NONE });
+ mValue = new int[1];
+ mRedSize = redSize;
+ mGreenSize = greenSize;
+ mBlueSize = blueSize;
+ mAlphaSize = alphaSize;
+ mDepthSize = depthSize;
+ mStencilSize = stencilSize;
+ }
+
+ @Override
+ EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) {
+ for (EGLConfig config : configs) {
+ int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
+ int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
+ if (d >= mDepthSize && s >= mStencilSize) {
+ int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
+ int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
+ int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
+ int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
+ if (r == mRedSize && g == mGreenSize && b == mBlueSize && a >= mAlphaSize) {
+ return config;
+ }
+ }
+ }
+ return null;
+ }
+
+ private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config,
+ int attribute, int defaultValue) {
+ if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
+ return mValue[0];
+ }
+
+ return defaultValue;
+ }
+ }
+ }
+
+ /**
+ * Hardware renderer using OpenGL ES 2.0.
+ */
+ static class Gl20Renderer extends GlRenderer {
+ private GLES20Canvas mGlCanvas;
+
+ Gl20Renderer(boolean translucent) {
+ super(2, translucent);
+ }
+
+ @Override
+ Canvas createCanvas() {
+ return mGlCanvas = new GLES20Canvas(mGl, mTranslucent);
+ }
+
+ @Override
+ void onPreDraw() {
+ mGlCanvas.onPreDraw();
+ }
+ }
+
+ /**
+ * Hardware renderer using OpenGL ES 1.0.
+ */
+ @SuppressWarnings({"deprecation"})
+ static class Gl10Renderer extends GlRenderer {
+ Gl10Renderer(boolean translucent) {
+ super(1, translucent);
+ }
+
+ @Override
+ Canvas createCanvas() {
+ return new Canvas(mGl);
+ }
+
+ @Override
+ void destroy() {
+ if (isEnabled()) {
+ nativeAbandonGlCaches();
+ }
+
+ super.destroy();
+ }
+
+ @Override
+ void onPreDraw() {
+ GL11 gl = (GL11) mGl;
+ gl.glDisable(GL_SCISSOR_TEST);
+ gl.glClearColor(0, 0, 0, 0);
+ gl.glClear(GL_COLOR_BUFFER_BIT);
+ gl.glEnable(GL_SCISSOR_TEST);
+ }
+ }
+
+ // Inform Skia to just abandon its texture cache IDs doesn't call glDeleteTextures
+ // Used only by the native Skia OpenGL ES 1.x implementation
+ private static native void nativeAbandonGlCaches();
+}
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
index e5985c1..479e757 100644
--- a/core/java/android/view/LayoutInflater.java
+++ b/core/java/android/view/LayoutInflater.java
@@ -16,15 +16,15 @@
package android.view;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.util.AttributeSet;
import android.util.Xml;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.HashMap;
@@ -71,11 +71,11 @@ public abstract class LayoutInflater {
private final Object[] mConstructorArgs = new Object[2];
- private static final Class[] mConstructorSignature = new Class[] {
+ private static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
- private static final HashMap<String, Constructor> sConstructorMap =
- new HashMap<String, Constructor>();
+ private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
+ new HashMap<String, Constructor<? extends View>>();
private HashMap<String, Boolean> mFilterMap;
@@ -97,6 +97,7 @@ public abstract class LayoutInflater {
*
* @return True if this class is allowed to be inflated, or false otherwise
*/
+ @SuppressWarnings("unchecked")
boolean onLoadClass(Class clazz);
}
@@ -379,7 +380,7 @@ public abstract class LayoutInflater {
+ "ViewGroup root and attachToRoot=true");
}
- rInflate(parser, root, attrs);
+ rInflate(parser, root, attrs, false);
} else {
// Temp is the root view that was found in the xml
View temp = createViewFromTag(name, attrs);
@@ -404,7 +405,7 @@ public abstract class LayoutInflater {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp
- rInflate(parser, temp, attrs);
+ rInflate(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
@@ -453,18 +454,18 @@ public abstract class LayoutInflater {
* @param name The full name of the class to be instantiated.
* @param attrs The XML attributes supplied for this instance.
*
- * @return View The newly instantied view, or null.
+ * @return View The newly instantiated view, or null.
*/
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
- Constructor constructor = sConstructorMap.get(name);
- Class clazz = null;
+ Constructor<? extends View> constructor = sConstructorMap.get(name);
+ Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
- prefix != null ? (prefix + name) : name);
+ prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
@@ -482,7 +483,7 @@ public abstract class LayoutInflater {
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
- prefix != null ? (prefix + name) : name);
+ prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
@@ -497,7 +498,7 @@ public abstract class LayoutInflater {
Object[] args = mConstructorArgs;
args[1] = attrs;
- return (View) constructor.newInstance(args);
+ return constructor.newInstance(args);
} catch (NoSuchMethodException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
@@ -506,6 +507,13 @@ public abstract class LayoutInflater {
ie.initCause(e);
throw ie;
+ } catch (ClassCastException e) {
+ // If loaded class is not a View subclass
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Class is not a View "
+ + (prefix != null ? (prefix + name) : name));
+ ie.initCause(e);
+ throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
@@ -519,7 +527,7 @@ public abstract class LayoutInflater {
}
/**
- * Throw an excpetion because the specified class is not allowed to be inflated.
+ * Throw an exception because the specified class is not allowed to be inflated.
*/
private void failNotAllowed(String name, String prefix, AttributeSet attrs) {
InflateException ie = new InflateException(attrs.getPositionDescription()
@@ -590,8 +598,8 @@ public abstract class LayoutInflater {
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
*/
- private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
- throws XmlPullParserException, IOException {
+ private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
+ boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
@@ -618,12 +626,12 @@ public abstract class LayoutInflater {
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
- rInflate(parser, view, attrs);
+ rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
- parent.onFinishInflate();
+ if (finishInflate) parent.onFinishInflate();
}
private void parseRequestFocus(XmlPullParser parser, View parent)
@@ -674,7 +682,7 @@ public abstract class LayoutInflater {
if (TAG_MERGE.equals(childName)) {
// Inflate all children.
- rInflate(childParser, parent, childAttrs);
+ rInflate(childParser, parent, childAttrs, false);
} else {
final View view = createViewFromTag(childName, childAttrs);
final ViewGroup group = (ViewGroup) parent;
@@ -699,7 +707,7 @@ public abstract class LayoutInflater {
}
// Inflate all children.
- rInflate(childParser, view, childAttrs);
+ rInflate(childParser, view, childAttrs, true);
// Attempt to override the included layout's android:id with the
// one set on the <include /> tag itself.
diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java
index 46c805c..4a966b5 100644
--- a/core/java/android/view/MenuInflater.java
+++ b/core/java/android/view/MenuInflater.java
@@ -16,9 +16,8 @@
package android.view;
-import com.android.internal.view.menu.MenuItemImpl;
-
import java.io.IOException;
+import java.lang.reflect.Method;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -30,6 +29,8 @@ import android.content.res.XmlResourceParser;
import android.util.AttributeSet;
import android.util.Xml;
+import com.android.internal.view.menu.MenuItemImpl;
+
/**
* This class is used to instantiate menu XML files into Menu objects.
* <p>
@@ -166,6 +167,41 @@ public class MenuInflater {
}
}
+ private static class InflatedOnMenuItemClickListener
+ implements MenuItem.OnMenuItemClickListener {
+ private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class };
+
+ private Context mContext;
+ private Method mMethod;
+
+ public InflatedOnMenuItemClickListener(Context context, String methodName) {
+ mContext = context;
+ Class c = context.getClass();
+ try {
+ mMethod = c.getMethod(methodName, PARAM_TYPES);
+ } catch (Exception e) {
+ InflateException ex = new InflateException(
+ "Couldn't resolve menu item onClick handler " + methodName +
+ " in class " + c.getName());
+ ex.initCause(e);
+ throw ex;
+ }
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ try {
+ if (mMethod.getReturnType() == Boolean.TYPE) {
+ return (Boolean) mMethod.invoke(mContext, item);
+ } else {
+ mMethod.invoke(mContext, item);
+ return true;
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
/**
* State for the current menu.
* <p>
@@ -205,6 +241,16 @@ public class MenuInflater {
private boolean itemVisible;
private boolean itemEnabled;
+ /**
+ * Sync to attrs.xml enum, values in MenuItem:
+ * - 0: never
+ * - 1: ifRoom
+ * - 2: always
+ */
+ private int itemShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
+
+ private String itemListenerMethodName;
+
private static final int defaultGroupId = NO_ID;
private static final int defaultItemId = NO_ID;
private static final int defaultItemCategory = 0;
@@ -276,6 +322,8 @@ public class MenuInflater {
itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
+ itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, 0);
+ itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick);
a.recycle();
@@ -298,10 +346,19 @@ public class MenuInflater {
.setTitleCondensed(itemTitleCondensed)
.setIcon(itemIconResId)
.setAlphabeticShortcut(itemAlphabeticShortcut)
- .setNumericShortcut(itemNumericShortcut);
+ .setNumericShortcut(itemNumericShortcut)
+ .setShowAsAction(itemShowAsAction);
+
+ if (itemListenerMethodName != null) {
+ item.setOnMenuItemClickListener(
+ new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName));
+ }
- if (itemCheckable >= 2) {
- ((MenuItemImpl) item).setExclusiveCheckable(true);
+ if (item instanceof MenuItemImpl) {
+ MenuItemImpl impl = (MenuItemImpl) item;
+ if (itemCheckable >= 2) {
+ impl.setExclusiveCheckable(true);
+ }
}
}
diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java
index fcebec5..bfa349c 100644
--- a/core/java/android/view/MenuItem.java
+++ b/core/java/android/view/MenuItem.java
@@ -31,6 +31,21 @@ import android.view.View.OnCreateContextMenuListener;
* For a feature set of specific menu types, see {@link Menu}.
*/
public interface MenuItem {
+ /*
+ * These should be kept in sync with attrs.xml enum constants for showAsAction
+ */
+ /** Never show this item as a button in an Action Bar. */
+ public static final int SHOW_AS_ACTION_NEVER = 0;
+ /** Show this item as a button in an Action Bar if the system decides there is room for it. */
+ public static final int SHOW_AS_ACTION_IF_ROOM = 1;
+ /**
+ * Always show this item as a button in an Action Bar.
+ * Use sparingly! If too many items are set to always show in the Action Bar it can
+ * crowd the Action Bar and degrade the user experience on devices with smaller screens.
+ * A good rule of thumb is to have no more than 2 items set to always show at a time.
+ */
+ public static final int SHOW_AS_ACTION_ALWAYS = 2;
+
/**
* Interface definition for a callback to be invoked when a menu item is
* clicked.
@@ -381,4 +396,13 @@ public interface MenuItem {
* menu item to the menu. This can be null.
*/
public ContextMenuInfo getMenuInfo();
+
+ /**
+ * Sets how this item should display in the presence of an Action Bar.
+ *
+ * @param actionEnum How the item should display. One of
+ *
+ * @see android.app.ActionBar
+ */
+ public void setShowAsAction(int actionEnum);
} \ No newline at end of file
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index ae8c21d..35e229a 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -722,7 +722,7 @@ public final class MotionEvent implements Parcelable {
*
* @param pointerId The identifier of the pointer to be found.
* @return Returns either the index of the pointer (for use with
- * {@link #getX(int) et al.), or -1 if there is no data available for
+ * {@link #getX(int)} et al.), or -1 if there is no data available for
* that pointer identifier.
*/
public final int findPointerIndex(int pointerId) {
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index c469bcc..54cb4ca 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -543,6 +543,9 @@ public class SurfaceView extends View {
}
if (creating || formatChanged || sizeChanged
|| visibleChanged || realSizeChanged) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight);
+ }
}
if (redrawNeeded) {
for (SurfaceHolder.Callback c : callbacks) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 329b2e7..0831fb1 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -887,6 +887,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
public static final int HAPTIC_FEEDBACK_ENABLED = 0x10000000;
/**
+ * <p>Indicates that the view hierarchy should stop saving state when
+ * it reaches this view. If state saving is initiated immediately at
+ * the view, it will be allowed.
+ * {@hide}
+ */
+ static final int PARENT_SAVE_DISABLED = 0x20000000;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for PARENT_SAVE_DISABLED.</p>
+ * {@hide}
+ */
+ static final int PARENT_SAVE_DISABLED_MASK = 0x20000000;
+
+ /**
* View flag indicating whether {@link #addFocusables(ArrayList, int, int)}
* should add all focusable Views regardless if they are focusable in touch mode.
*/
@@ -3352,6 +3366,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
/**
+ * Indicates whether the entire hierarchy under this view will save its
+ * state when a state saving traversal occurs from its parent. The default
+ * is true; if false, these views will not be saved unless
+ * {@link #saveHierarchyState(SparseArray)} is called directly on this view.
+ *
+ * @return Returns true if the view state saving from parent is enabled, else false.
+ *
+ * @see #setSaveFromParentEnabled(boolean)
+ */
+ public boolean isSaveFromParentEnabled() {
+ return (mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED;
+ }
+
+ /**
+ * Controls whether the entire hierarchy under this view will save its
+ * state when a state saving traversal occurs from its parent. The default
+ * is true; if false, these views will not be saved unless
+ * {@link #saveHierarchyState(SparseArray)} is called directly on this view.
+ *
+ * @param enabled Set to false to <em>disable</em> state saving, or true
+ * (the default) to allow it.
+ *
+ * @see #isSaveFromParentEnabled()
+ * @see #setId(int)
+ * @see #onSaveInstanceState()
+ */
+ public void setSaveFromParentEnabled(boolean enabled) {
+ setFlags(enabled ? 0 : PARENT_SAVE_DISABLED, PARENT_SAVE_DISABLED_MASK);
+ }
+
+
+ /**
* Returns whether this View is able to take focus.
*
* @return True if this view can take focus, or false otherwise.
@@ -6812,16 +6858,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
- drawTop = topFadeStrength >= 0.0f;
+ drawTop = topFadeStrength > 0.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
- drawBottom = bottomFadeStrength >= 0.0f;
+ drawBottom = bottomFadeStrength > 0.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
- drawLeft = leftFadeStrength >= 0.0f;
+ drawLeft = leftFadeStrength > 0.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
- drawRight = rightFadeStrength >= 0.0f;
+ drawRight = rightFadeStrength > 0.0f;
}
saveCount = canvas.getSaveCount();
@@ -8558,13 +8604,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
if (mAttachInfo == null) {
return false;
}
- if ((flags&HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING) == 0
+ if ((flags & HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING) == 0
&& !isHapticFeedbackEnabled()) {
return false;
}
- return mAttachInfo.mRootCallbacks.performHapticFeedback(
- feedbackConstant,
- (flags&HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0);
+ return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant,
+ (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0);
}
/**
@@ -8635,8 +8680,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
- private static int[] stateSetUnion(final int[] stateSet1,
- final int[] stateSet2) {
+ private static int[] stateSetUnion(final int[] stateSet1, final int[] stateSet2) {
final int stateSet1Length = stateSet1.length;
final int stateSet2Length = stateSet2.length;
final int[] newSet = new int[stateSet1Length + stateSet2Length];
@@ -8674,7 +8718,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
-
+
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index e7b6c50..34777ce 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -86,10 +86,23 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
- // The current transformation to apply on the child being drawn
- private Transformation mChildTransformation;
+ /**
+ * A Transformation used when drawing children, to
+ * apply on the child being drawn.
+ */
+ private final Transformation mChildTransformation = new Transformation();
+
+ /**
+ * Used to track the current invalidation region.
+ */
private RectF mInvalidateRegion;
+ /**
+ * A Transformation used to calculate a correct
+ * invalidation area when the application is autoscaled.
+ */
+ private Transformation mInvalidationTransformation;
+
// Target of Motion events
private View mMotionTarget;
private final Rect mTempRect = new Rect();
@@ -1182,7 +1195,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
- children[i].dispatchSaveInstanceState(container);
+ View c = children[i];
+ if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
+ c.dispatchSaveInstanceState(container);
+ }
}
}
@@ -1207,7 +1223,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
- children[i].dispatchRestoreInstanceState(container);
+ View c = children[i];
+ if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
+ c.dispatchRestoreInstanceState(container);
+ }
}
}
@@ -1477,21 +1496,25 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
final int flags = mGroupFlags;
if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
- if (mChildTransformation != null) {
- mChildTransformation.clear();
- }
+ mChildTransformation.clear();
mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
}
Transformation transformToApply = null;
+ Transformation invalidationTransform;
final Animation a = child.getAnimation();
boolean concatMatrix = false;
+ boolean scalingRequired = false;
+ boolean caching = false;
+ if (!canvas.isHardwareAccelerated() &&
+ (flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||
+ (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {
+ caching = true;
+ if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
+ }
+
if (a != null) {
- if (mInvalidateRegion == null) {
- mInvalidateRegion = new RectF();
- }
- final RectF region = mInvalidateRegion;
final boolean initialized = a.isInitialized();
if (!initialized) {
@@ -1500,10 +1523,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
child.onAnimationStart();
}
- if (mChildTransformation == null) {
- mChildTransformation = new Transformation();
+ more = a.getTransformation(drawingTime, mChildTransformation,
+ scalingRequired ? mAttachInfo.mApplicationScale : 1f);
+ if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
+ if (mInvalidationTransformation == null) {
+ mInvalidationTransformation = new Transformation();
+ }
+ invalidationTransform = mInvalidationTransformation;
+ a.getTransformation(drawingTime, invalidationTransform, 1f);
+ } else {
+ invalidationTransform = mChildTransformation;
}
- more = a.getTransformation(drawingTime, mChildTransformation);
transformToApply = mChildTransformation;
concatMatrix = a.willChangeTransformationMatrix();
@@ -1520,7 +1550,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
invalidate(cl, ct, cr, cb);
}
} else {
- a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
+ if (mInvalidateRegion == null) {
+ mInvalidateRegion = new RectF();
+ }
+ final RectF region = mInvalidateRegion;
+ a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, invalidationTransform);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
@@ -1533,9 +1567,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
} else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
- if (mChildTransformation == null) {
- mChildTransformation = new Transformation();
- }
final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
if (hasTransform) {
final int transformType = mChildTransformation.getTransformationType();
@@ -1559,12 +1590,9 @@ 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) {
+ if (caching) {
cache = child.getDrawingCache(true);
- if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
}
final boolean hasNoCache = cache == null;
@@ -2870,7 +2898,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
*/
@ViewDebug.ExportedProperty(mapping = {
@ViewDebug.IntToString(from = PERSISTENT_NO_CACHE, to = "NONE"),
- @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ANIMATION"),
+ @ViewDebug.IntToString(from = PERSISTENT_ANIMATION_CACHE, to = "ANIMATION"),
@ViewDebug.IntToString(from = PERSISTENT_SCROLLING_CACHE, to = "SCROLLING"),
@ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ALL")
})
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 260bf7bc..a89e7f6 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -16,6 +16,7 @@
package android.view;
+import android.content.pm.ApplicationInfo;
import com.android.internal.view.BaseSurfaceHolder;
import com.android.internal.view.IInputMethodCallback;
import com.android.internal.view.IInputMethodSession;
@@ -33,7 +34,6 @@ import android.util.Config;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.EventLog;
-import android.util.Slog;
import android.util.SparseArray;
import android.view.View.MeasureSpec;
import android.view.accessibility.AccessibilityEvent;
@@ -52,15 +52,10 @@ import android.Manifest;
import android.media.AudioManager;
import java.lang.ref.WeakReference;
-import java.io.FileDescriptor;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
-import javax.microedition.khronos.egl.*;
-import javax.microedition.khronos.opengles.*;
-import static javax.microedition.khronos.opengles.GL10.*;
-
/**
* The top of a view hierarchy, implementing the needed protocol between View
* and the WindowManager. This is for the most part an internal implementation
@@ -68,14 +63,12 @@ import static javax.microedition.khronos.opengles.GL10.*;
*
* {@hide}
*/
-@SuppressWarnings({"EmptyCatchBlock"})
-public final class ViewRoot extends Handler implements ViewParent,
- View.AttachInfo.Callbacks {
+@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
+public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks {
private static final String TAG = "ViewRoot";
private static final boolean DBG = false;
private static final boolean SHOW_FPS = false;
- @SuppressWarnings({"ConstantConditionalExpression"})
- private static final boolean LOCAL_LOGV = false ? Config.LOGD : Config.LOGV;
+ private static final boolean LOCAL_LOGV = false;
/** @noinspection PointlessBooleanExpression*/
private static final boolean DEBUG_DRAW = false || LOCAL_LOGV;
private static final boolean DEBUG_LAYOUT = false || LOCAL_LOGV;
@@ -205,14 +198,7 @@ public final class ViewRoot extends Handler implements ViewParent,
int mCurScrollY;
Scroller mScroller;
- EGL10 mEgl;
- EGLDisplay mEglDisplay;
- EGLContext mEglContext;
- EGLSurface mEglSurface;
- GL11 mGL;
- Canvas mGlCanvas;
- boolean mUseGL;
- boolean mGlWanted;
+ HardwareRenderer mHwRenderer;
final ViewConfiguration mViewConfiguration;
@@ -242,8 +228,10 @@ public final class ViewRoot extends Handler implements ViewParent,
public ViewRoot(Context context) {
super();
- if (MEASURE_LATENCY && lt == null) {
- lt = new LatencyTimer(100, 1000);
+ if (MEASURE_LATENCY) {
+ if (lt == null) {
+ lt = new LatencyTimer(100, 1000);
+ }
}
// For debug only
@@ -331,122 +319,18 @@ public final class ViewRoot extends Handler implements ViewParent,
return false;
}
- private void initializeGL() {
- initializeGLInner();
- int err = mEgl.eglGetError();
- if (err != EGL10.EGL_SUCCESS) {
- // give-up on using GL
- destroyGL();
- mGlWanted = false;
- }
- }
-
- private void initializeGLInner() {
- final EGL10 egl = (EGL10) EGLContext.getEGL();
- mEgl = egl;
-
- /*
- * Get to the default display.
- */
- final EGLDisplay eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
- mEglDisplay = eglDisplay;
-
- /*
- * We can now initialize EGL for that display
- */
- int[] version = new int[2];
- egl.eglInitialize(eglDisplay, version);
-
- /*
- * Specify a configuration for our opengl session
- * and grab the first configuration that matches is
- */
- final int[] configSpec = {
- EGL10.EGL_RED_SIZE, 5,
- EGL10.EGL_GREEN_SIZE, 6,
- EGL10.EGL_BLUE_SIZE, 5,
- EGL10.EGL_DEPTH_SIZE, 0,
- EGL10.EGL_NONE
- };
- final EGLConfig[] configs = new EGLConfig[1];
- final int[] num_config = new int[1];
- egl.eglChooseConfig(eglDisplay, configSpec, configs, 1, num_config);
- final EGLConfig config = configs[0];
-
- /*
- * Create an OpenGL ES context. This must be done only once, an
- * OpenGL context is a somewhat heavy object.
- */
- final EGLContext context = egl.eglCreateContext(eglDisplay, config,
- EGL10.EGL_NO_CONTEXT, null);
- mEglContext = context;
-
- /*
- * Create an EGL surface we can render into.
- */
- final EGLSurface surface = egl.eglCreateWindowSurface(eglDisplay, config, mHolder, null);
- mEglSurface = surface;
-
- /*
- * Before we can issue GL commands, we need to make sure
- * the context is current and bound to a surface.
- */
- egl.eglMakeCurrent(eglDisplay, surface, surface, context);
-
- /*
- * Get to the appropriate GL interface.
- * This is simply done by casting the GL context to either
- * GL10 or GL11.
- */
- final GL11 gl = (GL11) context.getGL();
- mGL = gl;
- mGlCanvas = new Canvas(gl);
- mUseGL = true;
- }
-
- private void destroyGL() {
- // inform skia that the context is gone
- nativeAbandonGlCaches();
-
- mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
- EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
- mEgl.eglDestroyContext(mEglDisplay, mEglContext);
- mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
- mEgl.eglTerminate(mEglDisplay);
- mEglContext = null;
- mEglSurface = null;
- mEglDisplay = null;
- mEgl = null;
- mGlCanvas = null;
- mGL = null;
- mUseGL = false;
- }
-
- private void checkEglErrors() {
- if (mUseGL) {
- int err = mEgl.eglGetError();
- if (err != EGL10.EGL_SUCCESS) {
- // something bad has happened revert to
- // normal rendering.
- destroyGL();
- if (err != EGL11.EGL_CONTEXT_LOST) {
- // we'll try again if it was context lost
- mGlWanted = false;
- }
- }
- }
- }
-
/**
* We have one child
*/
- public void setView(View view, WindowManager.LayoutParams attrs,
- View panelParentView) {
+ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mWindowAttributes.copyFrom(attrs);
attrs = mWindowAttributes;
+
+ enableHardwareAcceleration(view, attrs);
+
if (view instanceof RootViewSurfaceTaker) {
mSurfaceHolderCallback =
((RootViewSurfaceTaker)view).willYouTakeTheSurface();
@@ -576,6 +460,20 @@ public final class ViewRoot extends Handler implements ViewParent,
}
}
+ private void enableHardwareAcceleration(View view, WindowManager.LayoutParams attrs) {
+ // Only enable hardware acceleration if we are not in the system process
+ // The window manager creates ViewRoots to display animated preview windows
+ // of launching apps and we don't want those to be hardware accelerated
+ if (Process.myUid() != Process.SYSTEM_UID) {
+ // Try to enable hardware acceleration if requested
+ if ((view.getContext().getApplicationInfo().flags &
+ ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
+ final boolean translucent = attrs.format != PixelFormat.OPAQUE;
+ mHwRenderer = HardwareRenderer.createGlRenderer(2, translucent);
+ }
+ }
+ }
+
public View getView() {
return mView;
}
@@ -732,8 +630,6 @@ 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;
@@ -781,8 +677,8 @@ public final class ViewRoot extends Handler implements ViewParent,
attachInfo.mWindowVisibility = viewVisibility;
host.dispatchWindowVisibilityChanged(viewVisibility);
if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
- if (mUseGL) {
- destroyGL();
+ if (mHwRenderer != null) {
+ mHwRenderer.destroy();
}
}
if (viewVisibility == View.GONE) {
@@ -898,10 +794,12 @@ public final class ViewRoot extends Handler implements ViewParent,
final boolean computesInternalInsets =
attachInfo.mTreeObserver.hasComputeInternalInsetsListeners();
+
boolean insetsPending = false;
int relayoutResult = 0;
- if (mFirst || windowShouldResize || insetsChanged
- || viewVisibilityChanged || params != null) {
+
+ if (mFirst || windowShouldResize || insetsChanged ||
+ viewVisibilityChanged || params != null) {
if (viewVisibility == View.VISIBLE) {
// If this window is giving internal insets to the window
@@ -913,26 +811,19 @@ public final class ViewRoot extends Handler implements ViewParent,
// window, waiting until we can finish laying out this window
// and get back to the window manager with the ultimately
// computed insets.
- insetsPending = computesInternalInsets
- && (mFirst || viewVisibilityChanged);
-
- if (mWindowAttributes.memoryType == WindowManager.LayoutParams.MEMORY_TYPE_GPU) {
- if (params == null) {
- params = mWindowAttributes;
- }
- mGlWanted = true;
- }
+ insetsPending = computesInternalInsets && (mFirst || viewVisibilityChanged);
}
if (mSurfaceHolder != null) {
mSurfaceHolder.mSurfaceLock.lock();
mDrawingAllowed = true;
}
-
- boolean initialized = false;
+
+ boolean hwIntialized = false;
boolean contentInsetsChanged = false;
boolean visibleInsetsChanged;
boolean hadSurface = mSurface.isValid();
+
try {
int fl = 0;
if (params != null) {
@@ -992,9 +883,8 @@ public final class ViewRoot extends Handler implements ViewParent,
fullRedrawNeeded = true;
mPreviousTransparentRegion.setEmpty();
- if (mGlWanted && !mUseGL) {
- initializeGL();
- initialized = mGlCanvas != null;
+ if (mHwRenderer != null) {
+ hwIntialized = mHwRenderer.initialize(mHolder);
}
}
} else if (!mSurface.isValid()) {
@@ -1072,9 +962,8 @@ public final class ViewRoot extends Handler implements ViewParent,
}
}
- if (initialized) {
- mGlCanvas.setViewport((int) (mWidth * appScale + 0.5f),
- (int) (mHeight * appScale + 0.5f));
+ if (hwIntialized) {
+ mHwRenderer.setup(mWidth, mHeight, mAttachInfo);
}
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
@@ -1347,7 +1236,8 @@ public final class ViewRoot extends Handler implements ViewParent,
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
- for (int i=0; i<sFirstDrawHandlers.size(); i++) {
+ final int count = sFirstDrawHandlers.size();
+ for (int i = 0; i< count; i++) {
post(sFirstDrawHandlers.get(i));
}
}
@@ -1381,53 +1271,16 @@ public final class ViewRoot extends Handler implements ViewParent,
return;
}
- if (mUseGL) {
+ if (mHwRenderer != null && mHwRenderer.isEnabled()) {
if (!dirty.isEmpty()) {
- Canvas canvas = mGlCanvas;
- 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;
-
- int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
- try {
- canvas.translate(0, -yoff);
- if (mTranslator != null) {
- mTranslator.translateCanvas(canvas);
- }
- canvas.setScreenDensity(scalingRequired
- ? DisplayMetrics.DENSITY_DEVICE : 0);
- mView.draw(canvas);
- if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
- mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
- }
- } finally {
- canvas.restoreToCount(saveCount);
- }
-
- mAttachInfo.mIgnoreDirtyState = false;
-
- mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
- checkEglErrors();
-
- if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
- int now = (int)SystemClock.elapsedRealtime();
- if (sDrawTime != 0) {
- nativeShowFPS(canvas, now - sDrawTime);
- }
- sDrawTime = now;
- }
- }
+ mHwRenderer.draw(mView, mAttachInfo, yoff);
}
+
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
+
return;
}
@@ -1739,8 +1592,6 @@ public final class ViewRoot extends Handler implements ViewParent,
}
void dispatchDetachedFromWindow() {
- if (Config.LOGV) Log.v("ViewRoot", "Detaching in " + this + " of " + mSurface);
-
if (mView != null) {
mView.dispatchDetachedFromWindow();
}
@@ -1749,8 +1600,8 @@ public final class ViewRoot extends Handler implements ViewParent,
mAttachInfo.mRootView = null;
mAttachInfo.mSurface = null;
- if (mUseGL) {
- destroyGL();
+ if (mHwRenderer != null) {
+ mHwRenderer.destroy();
}
mSurface.release();
@@ -1934,18 +1785,8 @@ public final class ViewRoot extends Handler implements ViewParent,
boolean inTouchMode = msg.arg2 != 0;
ensureTouchModeLocally(inTouchMode);
- if (mGlWanted) {
- checkEglErrors();
- // we lost the gl context, so recreate it.
- if (mGlWanted && !mUseGL) {
- initializeGL();
- if (mGlCanvas != null) {
- float appScale = mAttachInfo.mApplicationScale;
- mGlCanvas.setViewport(
- (int) (mWidth * appScale + 0.5f),
- (int) (mHeight * appScale + 0.5f));
- }
- }
+ if (mHwRenderer != null) {
+ mHwRenderer.initializeIfNeeded(mWidth, mHeight, mAttachInfo, mHolder);
}
}
@@ -1995,8 +1836,7 @@ public final class ViewRoot extends Handler implements ViewParent,
if ((event.getFlags()&KeyEvent.FLAG_FROM_SYSTEM) != 0) {
// The IME is trying to say this event is from the
// system! Bad bad bad!
- event = KeyEvent.changeFlags(event,
- event.getFlags()&~KeyEvent.FLAG_FROM_SYSTEM);
+ event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM);
}
deliverKeyEventToViewHierarchy((KeyEvent)msg.obj, false);
} break;
@@ -2479,8 +2319,7 @@ public final class ViewRoot extends Handler implements ViewParent,
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.
- boolean handled = mView != null
- ? mView.dispatchKeyEventPreIme(event) : true;
+ boolean handled = mView == null || mView.dispatchKeyEventPreIme(event);
if (handled) {
if (sendDone) {
if (LOCAL_LOGV) Log.v(
@@ -2514,7 +2353,6 @@ public final class ViewRoot extends Handler implements ViewParent,
final boolean sendDone = seq >= 0;
if (!handled) {
deliverKeyEventToViewHierarchy(event, sendDone);
- return;
} else if (sendDone) {
if (LOCAL_LOGV) Log.v(
"ViewRoot", "Telling window manager key is finished");
@@ -2715,7 +2553,7 @@ public final class ViewRoot extends Handler implements ViewParent,
void doDie() {
checkThread();
- if (Config.LOGV) Log.v("ViewRoot", "DIE in " + this + " of " + mSurface);
+ if (LOCAL_LOGV) Log.v("ViewRoot", "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mAdded && !mFirst) {
int viewVisibility = mView.getVisibility();
@@ -2798,13 +2636,12 @@ public final class ViewRoot extends Handler implements ViewParent,
if (event.getAction() == KeyEvent.ACTION_DOWN) {
//noinspection ConstantConditions
if (false && event.getKeyCode() == KeyEvent.KEYCODE_CAMERA) {
- if (Config.LOGD) Log.d("keydisp",
- "===================================================");
- if (Config.LOGD) Log.d("keydisp", "Focused view Hierarchy is:");
+ if (DBG) Log.d("keydisp", "===================================================");
+ if (DBG) Log.d("keydisp", "Focused view Hierarchy is:");
+
debug();
- if (Config.LOGD) Log.d("keydisp",
- "===================================================");
+ if (DBG) Log.d("keydisp", "===================================================");
}
}
@@ -3374,8 +3211,4 @@ public final class ViewRoot extends Handler implements ViewParent,
}
private static native void nativeShowFPS(Canvas canvas, int durationMillis);
-
- // inform skia to just abandon its texture cache IDs
- // doesn't call glDeleteTextures
- private static native void nativeAbandonGlCaches();
}
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 11c09c1..be681cc 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -61,6 +61,13 @@ public abstract class Window {
@hide
*/
public static final int FEATURE_OPENGL = 8;
+ /**
+ * Flag for enabling the Action Bar.
+ * This is enabled by default for some devices. The Action Bar
+ * replaces the title bar and provides an alternate location
+ * for an on-screen menu button on some devices.
+ */
+ public static final int FEATURE_ACTION_BAR = 9;
/** Flag for setting the progress bar's visibility to VISIBLE */
public static final int PROGRESS_VISIBILITY_ON = -1;
/** Flag for setting the progress bar's visibility to GONE */
@@ -817,6 +824,8 @@ public abstract class Window {
public abstract void togglePanel(int featureId, KeyEvent event);
+ public abstract void invalidatePanelMenu(int featureId);
+
public abstract boolean performPanelShortcut(int featureId,
int keyCode,
KeyEvent event,
@@ -996,6 +1005,16 @@ public abstract class Window {
{
return mFeatures;
}
+
+ /**
+ * Query for the availability of a certain feature.
+ *
+ * @param feature The feature ID to check
+ * @return true if the feature is enabled, false otherwise.
+ */
+ public boolean hasFeature(int feature) {
+ return (getFeatures() & (1 << feature)) != 0;
+ }
/**
* Return the feature bits that are being implemented by this Window.
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index c22f991..fc61700 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -622,6 +622,7 @@ public final class AccessibilityEvent implements Parcelable {
mPackageName = null;
mContentDescription = null;
mBeforeText = null;
+ mParcelableData = null;
mText.clear();
}
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 0186270..f406da9 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -94,7 +94,9 @@ public final class AccessibilityManager {
public static AccessibilityManager getInstance(Context context) {
synchronized (sInstanceSync) {
if (sInstance == null) {
- sInstance = new AccessibilityManager(context);
+ IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
+ IAccessibilityManager service = IAccessibilityManager.Stub.asInterface(iBinder);
+ sInstance = new AccessibilityManager(context, service);
}
}
return sInstance;
@@ -104,13 +106,16 @@ public final class AccessibilityManager {
* Create an instance.
*
* @param context A {@link Context}.
+ * @param service An interface to the backing service.
+ *
+ * @hide
*/
- private AccessibilityManager(Context context) {
+ public AccessibilityManager(Context context, IAccessibilityManager service) {
mHandler = new MyHandler(context.getMainLooper());
- IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
- mService = IAccessibilityManager.Stub.asInterface(iBinder);
+ mService = service;
+
try {
- mService.addClient(mClient);
+ mIsEnabled = mService.addClient(mClient);
} catch (RemoteException re) {
Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
}
@@ -128,6 +133,18 @@ public final class AccessibilityManager {
}
/**
+ * Returns the client interface this instance registers in
+ * the centralized accessibility manager service.
+ *
+ * @return The client.
+ *
+ * @hide
+ */
+ public IAccessibilityManagerClient getClient() {
+ return (IAccessibilityManagerClient) mClient.asBinder();
+ }
+
+ /**
* Sends an {@link AccessibilityEvent}. If this {@link AccessibilityManager} is not
* enabled the call is a NOOP.
*
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 32788be..7633569 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -29,7 +29,7 @@ import android.content.pm.ServiceInfo;
*/
interface IAccessibilityManager {
- void addClient(IAccessibilityManagerClient client);
+ boolean addClient(IAccessibilityManagerClient client);
boolean sendAccessibilityEvent(in AccessibilityEvent uiEvent);
diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java
index 349b7e5..f3392d9 100644
--- a/core/java/android/view/animation/Animation.java
+++ b/core/java/android/view/animation/Animation.java
@@ -174,7 +174,13 @@ public abstract class Animation implements Cloneable {
* Desired Z order mode during animation.
*/
private int mZAdjustment;
-
+
+ /**
+ * scalefactor to apply to pivot points, etc. during animation. Subclasses retrieve the
+ * value via getScaleFactor().
+ */
+ private float mScaleFactor = 1f;
+
/**
* Don't animate the wallpaper.
*/
@@ -553,6 +559,19 @@ public abstract class Animation implements Cloneable {
}
/**
+ * The scale factor is set by the call to <code>getTransformation</code>. Overrides of
+ * {@link #getTransformation(long, Transformation, float)} will get this value
+ * directly. Overrides of {@link #applyTransformation(float, Transformation)} can
+ * call this method to get the value.
+ *
+ * @return float The scale factor that should be applied to pre-scaled values in
+ * an Animation such as the pivot points in {@link ScaleAnimation} and {@link RotateAnimation}.
+ */
+ protected float getScaleFactor() {
+ return mScaleFactor;
+ }
+
+ /**
* If detachWallpaper is true, and this is a window animation of a window
* that has a wallpaper background, then the window will be detached from
* the wallpaper while it runs. That is, the animation will only be applied
@@ -735,6 +754,7 @@ public abstract class Animation implements Cloneable {
* @return True if the animation is still running
*/
public boolean getTransformation(long currentTime, Transformation outTransformation) {
+
if (mStartTime == -1) {
mStartTime = currentTime;
}
@@ -806,6 +826,24 @@ public abstract class Animation implements Cloneable {
return mMore;
}
+
+ /**
+ * Gets the transformation to apply at a specified point in time. Implementations of this
+ * method should always replace the specified Transformation or document they are doing
+ * otherwise.
+ *
+ * @param currentTime Where we are in the animation. This is wall clock time.
+ * @param outTransformation A tranformation object that is provided by the
+ * caller and will be filled in by the animation.
+ * @param scale Scaling factor to apply to any inputs to the transform operation, such
+ * pivot points being rotated or scaled around.
+ * @return True if the animation is still running
+ */
+ public boolean getTransformation(long currentTime, Transformation outTransformation,
+ float scale) {
+ mScaleFactor = scale;
+ return getTransformation(currentTime, outTransformation);
+ }
/**
* <p>Indicates whether this animation has started or not.</p>
diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java
index 1546dcd..873ce53 100644
--- a/core/java/android/view/animation/AnimationSet.java
+++ b/core/java/android/view/animation/AnimationSet.java
@@ -312,7 +312,7 @@ public class AnimationSet extends Animation {
final Animation a = animations.get(i);
temp.clear();
- more = a.getTransformation(currentTime, temp) || more;
+ more = a.getTransformation(currentTime, temp, getScaleFactor()) || more;
t.compose(temp);
started = started || a.hasStarted();
diff --git a/core/java/android/view/animation/RotateAnimation.java b/core/java/android/view/animation/RotateAnimation.java
index 284ccce..58bf084 100644
--- a/core/java/android/view/animation/RotateAnimation.java
+++ b/core/java/android/view/animation/RotateAnimation.java
@@ -148,11 +148,12 @@ public class RotateAnimation extends Animation {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
-
+ float scale = getScaleFactor();
+
if (mPivotX == 0.0f && mPivotY == 0.0f) {
t.getMatrix().setRotate(degrees);
} else {
- t.getMatrix().setRotate(degrees, mPivotX, mPivotY);
+ t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale);
}
}
diff --git a/core/java/android/view/animation/ScaleAnimation.java b/core/java/android/view/animation/ScaleAnimation.java
index 1a56c8b..8537d42 100644
--- a/core/java/android/view/animation/ScaleAnimation.java
+++ b/core/java/android/view/animation/ScaleAnimation.java
@@ -161,6 +161,7 @@ public class ScaleAnimation extends Animation {
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
+ float scale = getScaleFactor();
if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
@@ -172,7 +173,7 @@ public class ScaleAnimation extends Animation {
if (mPivotX == 0 && mPivotY == 0) {
t.getMatrix().setScale(sx, sy);
} else {
- t.getMatrix().setScale(sx, sy, mPivotX, mPivotY);
+ t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
}
}
diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java
new file mode 100644
index 0000000..49ddc19
--- /dev/null
+++ b/core/java/android/webkit/AccessibilityInjector.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.webkit.WebViewCore.EventHub;
+
+/**
+ * This class injects accessibility into WebViews with disabled JavaScript or
+ * WebViews with enabled JavaScript but for which we have no accessibility
+ * script to inject.
+ */
+class AccessibilityInjector {
+
+ // Handle to the WebView this injector is associated with.
+ private final WebView mWebView;
+
+ /**
+ * Creates a new injector associated with a given VwebView.
+ *
+ * @param webView The associated WebView.
+ */
+ public AccessibilityInjector(WebView webView) {
+ mWebView = webView;
+ }
+
+ /**
+ * Processes a key down <code>event</code>.
+ *
+ * @return True if the event was processed.
+ */
+ public boolean onKeyEvent(KeyEvent event) {
+
+ // as a proof of concept let us do the simplest example
+
+ if (event.getAction() != KeyEvent.ACTION_UP) {
+ return false;
+ }
+
+ int keyCode = event.getKeyCode();
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_N:
+ modifySelection("extend", "forward", "sentence");
+ break;
+ case KeyEvent.KEYCODE_P:
+ modifySelection("extend", "backward", "sentence");
+ break;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when the <code>selectionString</code> has changed.
+ */
+ public void onSelectionStringChange(String selectionString) {
+ // put the selection string in an AccessibilityEvent and send it
+ AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ event.getText().add(selectionString);
+ mWebView.sendAccessibilityEventUnchecked(event);
+ }
+
+ /**
+ * Modifies the current selection.
+ *
+ * @param alter Specifies how to alter the selection.
+ * @param direction The direction in which to alter the selection.
+ * @param granularity The granularity of the selection modification.
+ */
+ private void modifySelection(String alter, String direction, String granularity) {
+ WebViewCore webViewCore = mWebView.getWebViewCore();
+
+ if (webViewCore == null) {
+ return;
+ }
+
+ WebViewCore.ModifySelectionData data = new WebViewCore.ModifySelectionData();
+ data.mAlter = alter;
+ data.mDirection = direction;
+ data.mGranularity = granularity;
+ webViewCore.sendMessage(EventHub.MODIFY_SELECTION, data);
+ }
+}
diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java
index 219a469..b021ded 100644
--- a/core/java/android/webkit/BrowserFrame.java
+++ b/core/java/android/webkit/BrowserFrame.java
@@ -72,6 +72,8 @@ class BrowserFrame extends Handler {
// queue has been cleared,they are ignored.
private boolean mBlockMessages = false;
+ private static String sDataDirectory = "";
+
// Is this frame the main frame?
private boolean mIsMainFrame;
@@ -224,6 +226,11 @@ class BrowserFrame extends Handler {
AssetManager am = context.getAssets();
nativeCreateFrame(w, am, proxy.getBackForwardList());
+ if (sDataDirectory.length() == 0) {
+ String dir = appContext.getFilesDir().getAbsolutePath();
+ sDataDirectory = dir.substring(0, dir.lastIndexOf('/'));
+ }
+
if (DebugFlags.BROWSER_FRAME) {
Log.v(LOGTAG, "BrowserFrame constructor: this=" + this);
}
@@ -294,6 +301,18 @@ class BrowserFrame extends Handler {
}
/**
+ * Saves the contents of the frame as a web archive.
+ *
+ * @param basename The filename where the archive should be placed.
+ * @param autoname If false, takes filename to be a file. If true, filename
+ * is assumed to be a directory in which a filename will be
+ * chosen according to the url of the current page.
+ */
+ /* package */ String saveWebArchive(String basename, boolean autoname) {
+ return nativeSaveWebArchive(basename, autoname);
+ }
+
+ /**
* Go back or forward the number of steps given.
* @param steps A negative or positive number indicating the direction
* and number of steps to move.
@@ -510,12 +529,21 @@ class BrowserFrame extends Handler {
private native String externalRepresentation();
/**
- * Retrieves the visual text of the current frame, puts it as the object for
+ * Retrieves the visual text of the frames, puts it as the object for
* the message and sends the message.
* @param callback the message to use to send the visual text
*/
public void documentAsText(Message callback) {
- callback.obj = documentAsText();;
+ StringBuilder text = new StringBuilder();
+ if (callback.arg1 != 0) {
+ // Dump top frame as text.
+ text.append(documentAsText());
+ }
+ if (callback.arg2 != 0) {
+ // Dump child frames as text.
+ text.append(childFramesAsText());
+ }
+ callback.obj = text.toString();
callback.sendToTarget();
}
@@ -524,6 +552,11 @@ class BrowserFrame extends Handler {
*/
private native String documentAsText();
+ /**
+ * Return the text drawn on the child frames as a string
+ */
+ private native String childFramesAsText();
+
/*
* This method is called by WebCore to inform the frame that
* the Javascript window object has been cleared.
@@ -617,6 +650,14 @@ class BrowserFrame extends Handler {
}
/**
+ * Called by JNI. Gets the applications data directory
+ * @return String The applications data directory
+ */
+ private static String getDataDirectory() {
+ return sDataDirectory;
+ }
+
+ /**
* Start loading a resource.
* @param loaderHandle The native ResourceLoader that is the target of the
* data.
@@ -785,11 +826,7 @@ class BrowserFrame extends Handler {
* @return The BrowserFrame object stored in the new WebView.
*/
private BrowserFrame createWindow(boolean dialog, boolean userGesture) {
- WebView w = mCallbackProxy.createWindow(dialog, userGesture);
- if (w != null) {
- return w.getWebViewCore().getBrowserFrame();
- }
- return null;
+ return mCallbackProxy.createWindow(dialog, userGesture);
}
/**
@@ -846,6 +883,7 @@ class BrowserFrame extends Handler {
private static final int FILE_UPLOAD_LABEL = 4;
private static final int RESET_LABEL = 5;
private static final int SUBMIT_LABEL = 6;
+ private static final int FILE_UPLOAD_NO_FILE_CHOSEN = 7;
String getRawResFilename(int id) {
int resid;
@@ -875,6 +913,10 @@ class BrowserFrame extends Handler {
return mContext.getResources().getString(
com.android.internal.R.string.submit);
+ case FILE_UPLOAD_NO_FILE_CHOSEN:
+ return mContext.getResources().getString(
+ com.android.internal.R.string.no_file_chosen);
+
default:
Log.e(LOGTAG, "getRawResFilename got incompatible resource ID");
return "";
@@ -1010,5 +1052,7 @@ class BrowserFrame extends Handler {
*/
private native HashMap getFormTextData();
+ private native String nativeSaveWebArchive(String basename, boolean autoname);
+
private native void nativeOrientationChanged(int orientation);
}
diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java
index 0e0e032..1b5651b 100644
--- a/core/java/android/webkit/CallbackProxy.java
+++ b/core/java/android/webkit/CallbackProxy.java
@@ -16,6 +16,7 @@
package android.webkit;
+import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -41,6 +42,7 @@ import com.android.internal.R;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
+import java.util.Map;
/**
* This class is a proxy class for handling WebCore -> UI thread messaging. All
@@ -112,6 +114,7 @@ class CallbackProxy extends Handler {
private static final int ADD_HISTORY_ITEM = 135;
private static final int HISTORY_INDEX_CHANGED = 136;
private static final int AUTH_CREDENTIALS = 137;
+ private static final int SET_INSTALLABLE_WEBAPP = 138;
// Message triggered by the client to resume execution
private static final int NOTIFY = 200;
@@ -500,18 +503,32 @@ class CallbackProxy extends Handler {
String url = msg.getData().getString("url");
if (!mWebChromeClient.onJsAlert(mWebView, url, message,
res)) {
+ // only display the alert dialog if the mContext is
+ // Activity and its window has the focus.
+ if (!(mContext instanceof Activity)
+ || !((Activity) mContext).hasWindowFocus()) {
+ res.cancel();
+ res.setReady();
+ break;
+ }
new AlertDialog.Builder(mContext)
.setTitle(getJsDialogTitle(url))
.setMessage(message)
.setPositiveButton(R.string.ok,
- new AlertDialog.OnClickListener() {
+ new DialogInterface.OnClickListener() {
public void onClick(
DialogInterface dialog,
int which) {
res.confirm();
}
})
- .setCancelable(false)
+ .setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ public void onCancel(
+ DialogInterface dialog) {
+ res.cancel();
+ }
+ })
.show();
}
res.setReady();
@@ -525,6 +542,14 @@ class CallbackProxy extends Handler {
String url = msg.getData().getString("url");
if (!mWebChromeClient.onJsConfirm(mWebView, url, message,
res)) {
+ // only display the alert dialog if the mContext is
+ // Activity and its window has the focus.
+ if (!(mContext instanceof Activity)
+ || !((Activity) mContext).hasWindowFocus()) {
+ res.cancel();
+ res.setReady();
+ break;
+ }
new AlertDialog.Builder(mContext)
.setTitle(getJsDialogTitle(url))
.setMessage(message)
@@ -565,6 +590,14 @@ class CallbackProxy extends Handler {
String url = msg.getData().getString("url");
if (!mWebChromeClient.onJsPrompt(mWebView, url, message,
defaultVal, res)) {
+ // only display the alert dialog if the mContext is
+ // Activity and its window has the focus.
+ if (!(mContext instanceof Activity)
+ || !((Activity) mContext).hasWindowFocus()) {
+ res.cancel();
+ res.setReady();
+ break;
+ }
final LayoutInflater factory = LayoutInflater
.from(mContext);
final View view = factory.inflate(R.layout.js_prompt,
@@ -616,6 +649,14 @@ class CallbackProxy extends Handler {
String url = msg.getData().getString("url");
if (!mWebChromeClient.onJsBeforeUnload(mWebView, url,
message, res)) {
+ // only display the alert dialog if the mContext is
+ // Activity and its window has the focus.
+ if (!(mContext instanceof Activity)
+ || !((Activity) mContext).hasWindowFocus()) {
+ res.cancel();
+ res.setReady();
+ break;
+ }
final String m = mContext.getString(
R.string.js_dialog_before_unload, message);
new AlertDialog.Builder(mContext)
@@ -725,7 +766,8 @@ class CallbackProxy extends Handler {
case OPEN_FILE_CHOOSER:
if (mWebChromeClient != null) {
- mWebChromeClient.openFileChooser((UploadFile) msg.obj);
+ UploadFileMessageData data = (UploadFileMessageData)msg.obj;
+ mWebChromeClient.openFileChooser(data.getUploadFile(), data.getAcceptType());
}
break;
@@ -750,6 +792,9 @@ class CallbackProxy extends Handler {
mWebView.setHttpAuthUsernamePassword(
host, realm, username, password);
break;
+ case SET_INSTALLABLE_WEBAPP:
+ mWebChromeClient.setInstallableWebApp();
+ break;
}
}
@@ -1087,10 +1132,15 @@ class CallbackProxy extends Handler {
public void onProgressChanged(int newProgress) {
// Synchronize so that mLatestProgress is up-to-date.
synchronized (this) {
- if (mWebChromeClient == null || mLatestProgress == newProgress) {
+ // update mLatestProgress even mWebChromeClient is null as
+ // WebView.getProgress() needs it
+ if (mLatestProgress == newProgress) {
return;
}
mLatestProgress = newProgress;
+ if (mWebChromeClient == null) {
+ return;
+ }
if (!mProgressUpdatePending) {
sendEmptyMessage(PROGRESS);
mProgressUpdatePending = true;
@@ -1098,7 +1148,7 @@ class CallbackProxy extends Handler {
}
}
- public WebView createWindow(boolean dialog, boolean userGesture) {
+ public BrowserFrame createWindow(boolean dialog, boolean userGesture) {
// Do an unsynchronized quick check to avoid posting if no callback has
// been set.
if (mWebChromeClient == null) {
@@ -1122,9 +1172,15 @@ class CallbackProxy extends Handler {
WebView w = transport.getWebView();
if (w != null) {
- w.getWebViewCore().initializeSubwindow();
+ WebViewCore core = w.getWebViewCore();
+ // If WebView.destroy() has been called, core may be null. Skip
+ // initialization in that case and return null.
+ if (core != null) {
+ core.initializeSubwindow();
+ return core.getBrowserFrame();
+ }
}
- return w;
+ return null;
}
public void onRequestFocus() {
@@ -1166,9 +1222,7 @@ class CallbackProxy extends Handler {
// for null.
WebHistoryItem i = mBackForwardList.getCurrentItem();
if (i != null) {
- if (precomposed || i.getTouchIconUrl() == null) {
- i.setTouchIconUrl(url);
- }
+ i.setTouchIconUrl(url, precomposed);
}
// Do an unsynchronized quick check to avoid posting if no callback has
// been set.
@@ -1426,6 +1480,24 @@ class CallbackProxy extends Handler {
sendMessage(msg);
}
+ private static class UploadFileMessageData {
+ private UploadFile mCallback;
+ private String mAcceptType;
+
+ public UploadFileMessageData(UploadFile uploadFile, String acceptType) {
+ mCallback = uploadFile;
+ mAcceptType = acceptType;
+ }
+
+ public UploadFile getUploadFile() {
+ return mCallback;
+ }
+
+ public String getAcceptType() {
+ return mAcceptType;
+ }
+ }
+
private class UploadFile implements ValueCallback<Uri> {
private Uri mValue;
public void onReceiveValue(Uri value) {
@@ -1442,13 +1514,14 @@ class CallbackProxy extends Handler {
/**
* Called by WebViewCore to open a file chooser.
*/
- /* package */ Uri openFileChooser() {
+ /* package */ Uri openFileChooser(String acceptType) {
if (mWebChromeClient == null) {
return null;
}
Message myMessage = obtainMessage(OPEN_FILE_CHOOSER);
UploadFile uploadFile = new UploadFile();
- myMessage.obj = uploadFile;
+ UploadFileMessageData data = new UploadFileMessageData(uploadFile, acceptType);
+ myMessage.obj = data;
synchronized (this) {
sendMessage(myMessage);
try {
@@ -1477,4 +1550,11 @@ class CallbackProxy extends Handler {
Message msg = obtainMessage(HISTORY_INDEX_CHANGED, index, 0, item);
sendMessage(msg);
}
+
+ void setInstallableWebApp() {
+ if (mWebChromeClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(SET_INSTALLABLE_WEBAPP));
+ }
}
diff --git a/core/java/android/webkit/GeolocationService.java b/core/java/android/webkit/GeolocationService.java
index 24306f4..91de1d8 100755
--- a/core/java/android/webkit/GeolocationService.java
+++ b/core/java/android/webkit/GeolocationService.java
@@ -45,14 +45,13 @@ final class GeolocationService implements LocationListener {
/**
* Constructor
+ * @param context The context from which we obtain the system service.
* @param nativeObject The native object to which this object will report position updates and
* errors.
*/
- public GeolocationService(long nativeObject) {
+ public GeolocationService(Context context, long nativeObject) {
mNativeObject = nativeObject;
// Register newLocationAvailable with platform service.
- ActivityThread thread = ActivityThread.systemMain();
- Context context = thread.getApplication();
mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (mLocationManager == null) {
Log.e(TAG, "Could not get location manager.");
@@ -62,9 +61,10 @@ final class GeolocationService implements LocationListener {
/**
* Start listening for location updates.
*/
- public void start() {
+ public boolean start() {
registerForLocationUpdates();
mIsRunning = true;
+ return mIsNetworkProviderAvailable || mIsGpsProviderAvailable;
}
/**
@@ -87,6 +87,8 @@ final class GeolocationService implements LocationListener {
// only unregister from all, then reregister with all but the GPS.
unregisterFromLocationUpdates();
registerForLocationUpdates();
+ // Check that the providers are still available after we re-register.
+ maybeReportError("The last location provider is no longer available");
}
}
}
@@ -156,11 +158,16 @@ final class GeolocationService implements LocationListener {
*/
private void registerForLocationUpdates() {
try {
- mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this);
- mIsNetworkProviderAvailable = true;
+ // Registration may fail if providers are not present on the device.
+ try {
+ mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this);
+ mIsNetworkProviderAvailable = true;
+ } catch(IllegalArgumentException e) { }
if (mIsGpsEnabled) {
- mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
- mIsGpsProviderAvailable = true;
+ try {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
+ mIsGpsProviderAvailable = true;
+ } catch(IllegalArgumentException e) { }
}
} catch(SecurityException e) {
Log.e(TAG, "Caught security exception registering for location updates from system. " +
@@ -173,6 +180,8 @@ final class GeolocationService implements LocationListener {
*/
private void unregisterFromLocationUpdates() {
mLocationManager.removeUpdates(this);
+ mIsNetworkProviderAvailable = false;
+ mIsGpsProviderAvailable = false;
}
/**
diff --git a/core/java/android/webkit/HTML5Audio.java b/core/java/android/webkit/HTML5Audio.java
new file mode 100644
index 0000000..d292881
--- /dev/null
+++ b/core/java/android/webkit/HTML5Audio.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnBufferingUpdateListener;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.media.MediaPlayer.OnSeekCompleteListener;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * <p>HTML5 support class for Audio.
+ */
+class HTML5Audio extends Handler
+ implements MediaPlayer.OnBufferingUpdateListener,
+ MediaPlayer.OnCompletionListener,
+ MediaPlayer.OnErrorListener,
+ MediaPlayer.OnPreparedListener,
+ MediaPlayer.OnSeekCompleteListener {
+ // Logging tag.
+ private static final String LOGTAG = "HTML5Audio";
+
+ private MediaPlayer mMediaPlayer;
+
+ // The C++ MediaPlayerPrivateAndroid object.
+ private int mNativePointer;
+
+ private static int IDLE = 0;
+ private static int INITIALIZED = 1;
+ private static int PREPARED = 2;
+ private static int STARTED = 4;
+ private static int COMPLETE = 5;
+ private static int PAUSED = 6;
+ private static int STOPPED = -2;
+ private static int ERROR = -1;
+
+ private int mState = IDLE;
+
+ private String mUrl;
+ private boolean mAskToPlay = false;
+
+ // Timer thread -> UI thread
+ private static final int TIMEUPDATE = 100;
+
+ // The spec says the timer should fire every 250 ms or less.
+ private static final int TIMEUPDATE_PERIOD = 250; // ms
+ // The timer for timeupate events.
+ // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate
+ private Timer mTimer;
+ private final class TimeupdateTask extends TimerTask {
+ public void run() {
+ HTML5Audio.this.obtainMessage(TIMEUPDATE).sendToTarget();
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case TIMEUPDATE: {
+ try {
+ if (mState != ERROR && mMediaPlayer.isPlaying()) {
+ int position = mMediaPlayer.getCurrentPosition();
+ nativeOnTimeupdate(position, mNativePointer);
+ }
+ } catch (IllegalStateException e) {
+ mState = ERROR;
+ }
+ }
+ }
+ }
+
+ // event listeners for MediaPlayer
+ // Those are called from the same thread we created the MediaPlayer
+ // (i.e. the webviewcore thread here)
+
+ // MediaPlayer.OnBufferingUpdateListener
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ nativeOnBuffering(percent, mNativePointer);
+ }
+
+ // MediaPlayer.OnCompletionListener;
+ public void onCompletion(MediaPlayer mp) {
+ resetMediaPlayer();
+ mState = IDLE;
+ nativeOnEnded(mNativePointer);
+ }
+
+ // MediaPlayer.OnErrorListener
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ mState = ERROR;
+ resetMediaPlayer();
+ mState = IDLE;
+ return false;
+ }
+
+ // MediaPlayer.OnPreparedListener
+ public void onPrepared(MediaPlayer mp) {
+ mState = PREPARED;
+ if (mTimer != null) {
+ mTimer.schedule(new TimeupdateTask(),
+ TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD);
+ }
+ nativeOnPrepared(mp.getDuration(), 0, 0, mNativePointer);
+ if (mAskToPlay) {
+ mAskToPlay = false;
+ play();
+ }
+ }
+
+ // MediaPlayer.OnSeekCompleteListener
+ public void onSeekComplete(MediaPlayer mp) {
+ nativeOnTimeupdate(mp.getCurrentPosition(), mNativePointer);
+ }
+
+
+ /**
+ * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object.
+ */
+ public HTML5Audio(int nativePtr) {
+ // Save the native ptr
+ mNativePointer = nativePtr;
+ resetMediaPlayer();
+ }
+
+ private void resetMediaPlayer() {
+ if (mMediaPlayer == null) {
+ mMediaPlayer = new MediaPlayer();
+ } else {
+ mMediaPlayer.reset();
+ }
+ mMediaPlayer.setOnBufferingUpdateListener(this);
+ mMediaPlayer.setOnCompletionListener(this);
+ mMediaPlayer.setOnErrorListener(this);
+ mMediaPlayer.setOnPreparedListener(this);
+ mMediaPlayer.setOnSeekCompleteListener(this);
+
+ if (mTimer != null) {
+ mTimer.cancel();
+ }
+ mTimer = new Timer();
+ mState = IDLE;
+ }
+
+ private void setDataSource(String url) {
+ mUrl = url;
+ try {
+ if (mState != IDLE) {
+ resetMediaPlayer();
+ }
+ mMediaPlayer.setDataSource(url);
+ mState = INITIALIZED;
+ mMediaPlayer.prepareAsync();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "couldn't load the resource: " + url + " exc: " + e);
+ resetMediaPlayer();
+ }
+ }
+
+ private void play() {
+ if ((mState == ERROR || mState == IDLE) && mUrl != null) {
+ resetMediaPlayer();
+ setDataSource(mUrl);
+ mAskToPlay = true;
+ }
+
+ if (mState >= PREPARED) {
+ mMediaPlayer.start();
+ mState = STARTED;
+ }
+ }
+
+ private void pause() {
+ if (mState == STARTED) {
+ if (mTimer != null) {
+ mTimer.purge();
+ }
+ mMediaPlayer.pause();
+ mState = PAUSED;
+ }
+ }
+
+ private void seek(int msec) {
+ if (mState >= PREPARED) {
+ mMediaPlayer.seekTo(msec);
+ }
+ }
+
+ private void teardown() {
+ mMediaPlayer.release();
+ mState = ERROR;
+ mNativePointer = 0;
+ }
+
+ private float getMaxTimeSeekable() {
+ return mMediaPlayer.getDuration() / 1000.0f;
+ }
+
+ private native void nativeOnBuffering(int percent, int nativePointer);
+ private native void nativeOnEnded(int nativePointer);
+ private native void nativeOnPrepared(int duration, int width, int height, int nativePointer);
+ private native void nativeOnTimeupdate(int position, int nativePointer);
+}
diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java
index e766693..ecad261 100644
--- a/core/java/android/webkit/JWebCoreJavaBridge.java
+++ b/core/java/android/webkit/JWebCoreJavaBridge.java
@@ -16,10 +16,12 @@
package android.webkit;
+import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
+import java.util.HashMap;
import java.util.Set;
final class JWebCoreJavaBridge extends Handler {
@@ -49,12 +51,15 @@ final class JWebCoreJavaBridge extends Handler {
/* package */
static final int REFRESH_PLUGINS = 100;
+ private HashMap<String, String> mContentUriToFilePathMap;
+
/**
* Construct a new JWebCoreJavaBridge to interface with
* WebCore timers and cookies.
*/
public JWebCoreJavaBridge() {
nativeConstructor();
+
}
@Override
@@ -267,6 +272,28 @@ final class JWebCoreJavaBridge extends Handler {
}
}
+ // Called on the WebCore thread through JNI.
+ private String resolveFilePathForContentUri(String uri) {
+ if (mContentUriToFilePathMap != null) {
+ String fileName = mContentUriToFilePathMap.get(uri);
+ if (fileName != null) {
+ return fileName;
+ }
+ }
+
+ // Failsafe fallback to just use the last path segment.
+ // (See OpenableColumns documentation in the SDK)
+ Uri jUri = Uri.parse(uri);
+ return jUri.getLastPathSegment();
+ }
+
+ public void storeFilePathForContentUri(String path, String contentUri) {
+ if (mContentUriToFilePathMap == null) {
+ mContentUriToFilePathMap = new HashMap<String, String>();
+ }
+ mContentUriToFilePathMap.put(contentUri, path);
+ }
+
private native void nativeConstructor();
private native void nativeFinalize();
private native void sharedTimerFired();
diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java
index c1ac180..6e9c70a 100644
--- a/core/java/android/webkit/MimeTypeMap.java
+++ b/core/java/android/webkit/MimeTypeMap.java
@@ -363,6 +363,7 @@ public class MimeTypeMap {
sMimeTypeMap.loadEntry("application/x-wais-source", "src");
sMimeTypeMap.loadEntry("application/x-wingz", "wz");
sMimeTypeMap.loadEntry("application/x-webarchive", "webarchive");
+ sMimeTypeMap.loadEntry("application/x-webarchive-xml", "webarchivexml");
sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt");
sMimeTypeMap.loadEntry("application/x-x509-user-cert", "crt");
sMimeTypeMap.loadEntry("application/x-xcf", "xcf");
diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java
index 598f20d..0f03258 100644
--- a/core/java/android/webkit/Network.java
+++ b/core/java/android/webkit/Network.java
@@ -16,7 +16,12 @@
package android.webkit;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.net.http.*;
import android.os.*;
import android.util.Log;
@@ -76,6 +81,19 @@ class Network {
*/
private HttpAuthHandler mHttpAuthHandler;
+ private Context mContext;
+
+ /**
+ * True if the currently used network connection is a roaming phone
+ * connection.
+ */
+ private boolean mRoaming;
+
+ /**
+ * Tracks if we are roaming.
+ */
+ private RoamingMonitor mRoamingMonitor;
+
/**
* @return The singleton instance of the network.
*/
@@ -107,6 +125,7 @@ class Network {
if (++sPlatformNotificationEnableRefCount == 1) {
if (sNetwork != null) {
sNetwork.mRequestQueue.enablePlatformNotifications();
+ sNetwork.monitorRoaming();
} else {
sPlatformNotifications = true;
}
@@ -121,6 +140,7 @@ class Network {
if (--sPlatformNotificationEnableRefCount == 0) {
if (sNetwork != null) {
sNetwork.mRequestQueue.disablePlatformNotifications();
+ sNetwork.stopMonitoringRoaming();
} else {
sPlatformNotifications = false;
}
@@ -136,12 +156,39 @@ class Network {
Assert.assertTrue(Thread.currentThread().
getName().equals(WebViewCore.THREAD_NAME));
}
+ mContext = context;
mSslErrorHandler = new SslErrorHandler();
mHttpAuthHandler = new HttpAuthHandler(this);
mRequestQueue = new RequestQueue(context);
}
+ private class RoamingMonitor extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()))
+ return;
+
+ NetworkInfo info = (NetworkInfo)intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ if (info != null)
+ mRoaming = info.isRoaming();
+ };
+ };
+
+ private void monitorRoaming() {
+ mRoamingMonitor = new RoamingMonitor();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(sNetwork.mRoamingMonitor, filter);
+ }
+
+ private void stopMonitoringRoaming() {
+ if (mRoamingMonitor != null) {
+ mContext.unregisterReceiver(mRoamingMonitor);
+ mRoamingMonitor = null;
+ }
+ }
+
/**
* Request a url from either the network or the file system.
* @param url The url to load.
@@ -170,6 +217,11 @@ class Network {
return false;
}
+ // If this is a prefetch, abort it if we're roaming.
+ if (mRoaming && headers.containsKey("X-Moz") && "prefetch".equals(headers.get("X-Moz"))) {
+ return false;
+ }
+
/* FIXME: this is lame. Pass an InputStream in, rather than
making this lame one here */
InputStream bodyProvider = null;
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index 1d5aac7..443a3b3 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -314,10 +314,34 @@ public class WebChromeClient {
/**
* Tell the client to open a file chooser.
* @param uploadFile A ValueCallback to set the URI of the file to upload.
- * onReceiveValue must be called to wake up the thread.
+ * onReceiveValue must be called to wake up the thread.a
+ * @param acceptType The value of the 'accept' attribute of the input tag
+ * associated with this file picker.
* @hide
*/
- public void openFileChooser(ValueCallback<Uri> uploadFile) {
+ public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType) {
uploadFile.onReceiveValue(null);
}
+
+ /**
+ * Tell the client that the selection has been initiated.
+ * @hide
+ */
+ public void onSelectionStart() {
+ }
+
+ /**
+ * Tell the client that the selection has been copied or canceled.
+ * @hide
+ */
+ public void onSelectionDone() {
+ }
+
+ /**
+ * Tell the client that the page being viewed is web app capable,
+ * i.e. has specified the fullscreen-web-app-capable meta tag.
+ * @hide
+ */
+ public void setInstallableWebApp() { }
+
}
diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java
index 428a59c..7c0e478 100644
--- a/core/java/android/webkit/WebHistoryItem.java
+++ b/core/java/android/webkit/WebHistoryItem.java
@@ -18,6 +18,9 @@ package android.webkit;
import android.graphics.Bitmap;
+import java.net.MalformedURLException;
+import java.net.URL;
+
/**
* A convenience class for accessing fields in an entry in the back/forward list
* of a WebView. Each WebHistoryItem is a snapshot of the requested history
@@ -39,8 +42,12 @@ public class WebHistoryItem implements Cloneable {
private Bitmap mFavicon;
// The pre-flattened data used for saving the state.
private byte[] mFlattenedData;
- // The apple-touch-icon url for use when adding the site to the home screen
- private String mTouchIconUrl;
+ // The apple-touch-icon url for use when adding the site to the home screen,
+ // as obtained from a <link> element in the page.
+ private String mTouchIconUrlFromLink;
+ // If no <link> is specified, this holds the default location of the
+ // apple-touch-icon.
+ private String mTouchIconUrlServerDefault;
// Custom client data that is not flattened or read by native code.
private Object mCustomData;
@@ -132,10 +139,28 @@ public class WebHistoryItem implements Cloneable {
/**
* Return the touch icon url.
+ * If no touch icon <link> tag was specified, returns
+ * <host>/apple-touch-icon.png. The DownloadTouchIcon class that
+ * attempts to retrieve the touch icon will handle the case where
+ * that file does not exist. An icon set by a <link> tag is always
+ * used in preference to an icon saved on the server.
* @hide
*/
public String getTouchIconUrl() {
- return mTouchIconUrl;
+ if (mTouchIconUrlFromLink != null) {
+ return mTouchIconUrlFromLink;
+ } else if (mTouchIconUrlServerDefault != null) {
+ return mTouchIconUrlServerDefault;
+ }
+
+ try {
+ URL url = new URL(mOriginalUrl);
+ mTouchIconUrlServerDefault = new URL(url.getProtocol(), url.getHost(), url.getPort(),
+ "/apple-touch-icon.png").toString();
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ return mTouchIconUrlServerDefault;
}
/**
@@ -171,11 +196,14 @@ public class WebHistoryItem implements Cloneable {
}
/**
- * Set the touch icon url.
+ * Set the touch icon url. Will not overwrite an icon that has been
+ * set already from a <link> tag, unless the new icon is precomposed.
* @hide
*/
- /*package*/ void setTouchIconUrl(String url) {
- mTouchIconUrl = url;
+ /*package*/ void setTouchIconUrl(String url, boolean precomposed) {
+ if (precomposed || mTouchIconUrlFromLink == null) {
+ mTouchIconUrlFromLink = url;
+ }
}
/**
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index b767f11..52e992b 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -19,6 +19,7 @@ package android.webkit;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.content.res.Configuration;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
@@ -107,7 +108,7 @@ public class WebSettings {
* Use with {@link #setCacheMode}.
*/
public static final int LOAD_NO_CACHE = 2;
-
+
/**
* Don't use the network, load from cache only.
* Use with {@link #setCacheMode}.
@@ -178,12 +179,14 @@ public class WebSettings {
private boolean mUseWideViewport = false;
private boolean mSupportMultipleWindows = false;
private boolean mShrinksStandaloneImagesToFit = false;
+ private long mMaximumDecodedImageSize = 0; // 0 means default
// HTML5 API flags
private boolean mAppCacheEnabled = false;
private boolean mDatabaseEnabled = false;
private boolean mDomStorageEnabled = false;
private boolean mWorkersEnabled = false; // only affects V8.
private boolean mGeolocationEnabled = true;
+ private boolean mXSSAuditorEnabled = false;
// HTML5 configuration parameters
private long mAppCacheMaxSize = Long.MAX_VALUE;
private String mAppCachePath = "";
@@ -207,6 +210,7 @@ public class WebSettings {
private boolean mBuiltInZoomControls = false;
private boolean mAllowFileAccess = true;
private boolean mLoadWithOverviewMode = false;
+ private boolean mEnableSmoothTransition = false;
// private WebSettings, not accessible by the host activity
static private int mDoubleTapToastCount = 3;
@@ -296,13 +300,13 @@ public class WebSettings {
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us)"
+ " AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0"
+ " Safari/530.17";
- private static final String IPHONE_USERAGENT =
+ private static final String IPHONE_USERAGENT =
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us)"
+ " AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0"
+ " Mobile/7A341 Safari/528.16";
private static Locale sLocale;
private static Object sLockForLocaleSettings;
-
+
/**
* Package constructor to prevent clients from creating a new settings
* instance.
@@ -327,6 +331,8 @@ public class WebSettings {
android.os.Process.myUid()) != PackageManager.PERMISSION_GRANTED;
}
+ private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US";
+
/**
* Looks at sLocale and returns current AcceptLanguage String.
* @return Current AcceptLanguage String.
@@ -336,32 +342,53 @@ public class WebSettings {
synchronized(sLockForLocaleSettings) {
locale = sLocale;
}
- StringBuffer buffer = new StringBuffer();
- final String language = locale.getLanguage();
- if (language != null) {
- buffer.append(language);
- final String country = locale.getCountry();
- if (country != null) {
- buffer.append("-");
- buffer.append(country);
- }
- }
- if (!locale.equals(Locale.US)) {
- buffer.append(", ");
- java.util.Locale us = Locale.US;
- if (us.getLanguage() != null) {
- buffer.append(us.getLanguage());
- final String country = us.getCountry();
- if (country != null) {
- buffer.append("-");
- buffer.append(country);
- }
+ StringBuilder buffer = new StringBuilder();
+ addLocaleToHttpAcceptLanguage(buffer, locale);
+
+ if (!Locale.US.equals(locale)) {
+ if (buffer.length() > 0) {
+ buffer.append(", ");
}
+ buffer.append(ACCEPT_LANG_FOR_US_LOCALE);
}
return buffer.toString();
}
-
+
+ /**
+ * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish,
+ * to new standard.
+ */
+ private static String convertObsoleteLanguageCodeToNew(String langCode) {
+ if (langCode == null) {
+ return null;
+ }
+ if ("iw".equals(langCode)) {
+ // Hebrew
+ return "he";
+ } else if ("in".equals(langCode)) {
+ // Indonesian
+ return "id";
+ } else if ("ji".equals(langCode)) {
+ // Yiddish
+ return "yi";
+ }
+ return langCode;
+ }
+
+ private static void addLocaleToHttpAcceptLanguage(StringBuilder builder,
+ Locale locale) {
+ String language = convertObsoleteLanguageCodeToNew(locale.getLanguage());
+ if (language != null) {
+ builder.append(language);
+ String country = locale.getCountry();
+ if (country != null) {
+ builder.append("-");
+ builder.append(country);
+ }
+ }
+ }
+
/**
* Looks at sLocale and mContext and returns current UserAgent String.
* @return Current UserAgent String.
@@ -379,11 +406,11 @@ public class WebSettings {
} else {
// default to "1.0"
buffer.append("1.0");
- }
+ }
buffer.append("; ");
final String language = locale.getLanguage();
if (language != null) {
- buffer.append(language.toLowerCase());
+ buffer.append(convertObsoleteLanguageCodeToNew(language));
final String country = locale.getCountry();
if (country != null) {
buffer.append("-");
@@ -406,11 +433,14 @@ public class WebSettings {
buffer.append(" Build/");
buffer.append(id);
}
+ String mobile = ((mContext.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK)
+ == Configuration.SCREENLAYOUT_SIZE_XLARGE) ? "" : "Mobile ";
final String base = mContext.getResources().getText(
com.android.internal.R.string.web_user_agent).toString();
- return String.format(base, buffer);
+ return String.format(base, buffer, mobile);
}
-
+
/**
* Enables dumping the pages navigation cache to a text file.
*/
@@ -426,6 +456,21 @@ public class WebSettings {
}
/**
+ * If WebView only supports touch, a different navigation model will be
+ * applied. Otherwise, the navigation to support both touch and keyboard
+ * will be used.
+ * @hide
+ public void setSupportTouchOnly(boolean touchOnly) {
+ mSupportTounchOnly = touchOnly;
+ }
+ */
+
+ boolean supportTouchOnly() {
+ // for debug only, use mLightTouchEnabled for mSupportTounchOnly
+ return mLightTouchEnabled;
+ }
+
+ /**
* Set whether the WebView supports zoom
*/
public void setSupportZoom(boolean support) {
@@ -447,14 +492,14 @@ public class WebSettings {
mBuiltInZoomControls = enabled;
mWebView.updateMultiTouchSupport(mContext);
}
-
+
/**
* Returns true if the zoom mechanism built into WebView is being used.
*/
public boolean getBuiltInZoomControls() {
return mBuiltInZoomControls;
}
-
+
/**
* Enable or disable file access within WebView. File access is enabled by
* default.
@@ -485,6 +530,25 @@ public class WebSettings {
}
/**
+ * Set whether the WebView will enable smooth transition while panning or
+ * zooming. If it is true, WebView will choose a solution to maximize the
+ * performance. e.g. the WebView's content may not be updated during the
+ * transition. If it is false, WebView will keep its fidelity. The default
+ * value is false.
+ */
+ public void setEnableSmoothTransition(boolean enable) {
+ mEnableSmoothTransition = enable;
+ }
+
+ /**
+ * Returns true if the WebView enables smooth transition while panning or
+ * zooming.
+ */
+ public boolean enableSmoothTransition() {
+ return mEnableSmoothTransition;
+ }
+
+ /**
* Store whether the WebView is saving form data.
*/
public void setSaveFormData(boolean save) {
@@ -984,8 +1048,8 @@ public class WebSettings {
private void verifyNetworkAccess() {
if (!mBlockNetworkLoads) {
- if (mContext.checkPermission("android.permission.INTERNET",
- android.os.Process.myPid(), android.os.Process.myUid()) !=
+ if (mContext.checkPermission("android.permission.INTERNET",
+ android.os.Process.myPid(), android.os.Process.myUid()) !=
PackageManager.PERMISSION_GRANTED) {
throw new SecurityException
("Permission denied - " +
@@ -1011,6 +1075,7 @@ public class WebSettings {
* @deprecated This method has been deprecated in favor of
* {@link #setPluginState}
*/
+ @Deprecated
public synchronized void setPluginsEnabled(boolean flag) {
setPluginState(PluginState.ON);
}
@@ -1176,6 +1241,18 @@ public class WebSettings {
}
/**
+ * Sets whether XSS Auditor is enabled.
+ * @param flag Whether XSS Auditor should be enabled.
+ * @hide Only used by LayoutTestController.
+ */
+ public synchronized void setXSSAuditorEnabled(boolean flag) {
+ if (mXSSAuditorEnabled != flag) {
+ mXSSAuditorEnabled = flag;
+ postSync();
+ }
+ }
+
+ /**
* Return true if javascript is enabled. <b>Note: The default is false.</b>
* @return True if javascript is enabled.
*/
@@ -1188,6 +1265,7 @@ public class WebSettings {
* @return True if plugins are enabled.
* @deprecated This method has been replaced by {@link #getPluginState}
*/
+ @Deprecated
public synchronized boolean getPluginsEnabled() {
return mPluginState == PluginState.ON;
}
@@ -1256,7 +1334,7 @@ public class WebSettings {
public synchronized void setUserAgentString(String ua) {
if (ua == null || ua.length() == 0) {
synchronized(sLockForLocaleSettings) {
- Locale currentLocale = Locale.getDefault();
+ Locale currentLocale = Locale.getDefault();
if (!sLocale.equals(currentLocale)) {
sLocale = currentLocale;
mAcceptLanguage = getCurrentAcceptLanguage();
@@ -1311,11 +1389,11 @@ public class WebSettings {
}
return mAcceptLanguage;
}
-
+
/**
* Tell the WebView whether it needs to set a node to have focus when
* {@link WebView#requestFocus(int, android.graphics.Rect)} is called.
- *
+ *
* @param flag
*/
public void setNeedInitialFocus(boolean flag) {
@@ -1342,7 +1420,7 @@ public class WebSettings {
EventHandler.PRIORITY));
}
}
-
+
/**
* Override the way the cache is used. The way the cache is used is based
* on the navigation option. For a normal page load, the cache is checked
@@ -1356,7 +1434,7 @@ public class WebSettings {
mOverrideCacheMode = mode;
}
}
-
+
/**
* Return the current setting for overriding the cache mode. For a full
* description, see the {@link #setCacheMode(int)} function.
@@ -1364,7 +1442,7 @@ public class WebSettings {
public int getCacheMode() {
return mOverrideCacheMode;
}
-
+
/**
* If set, webkit alternately shrinks and expands images viewed outside
* of an HTML page to fit the screen. This conflicts with attempts by
@@ -1379,6 +1457,19 @@ public class WebSettings {
}
}
+ /**
+ * Specify the maximum decoded image size. The default is
+ * 2 megs for small memory devices and 8 megs for large memory devices.
+ * @param size The maximum decoded size, or zero to set to the default.
+ * @hide pending api council approval
+ */
+ public void setMaximumDecodedImageSize(long size) {
+ if (mMaximumDecodedImageSize != size) {
+ mMaximumDecodedImageSize = size;
+ postSync();
+ }
+ }
+
int getDoubleTapToastCount() {
return mDoubleTapToastCount;
}
diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java
index 19abec1..eb36b5d 100644
--- a/core/java/android/webkit/WebTextView.java
+++ b/core/java/android/webkit/WebTextView.java
@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.InputFilter;
+import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextPaint;
@@ -300,6 +301,33 @@ import java.util.ArrayList;
return connection;
}
+ /**
+ * In general, TextView makes a call to InputMethodManager.updateSelection
+ * in onDraw. However, in the general case of WebTextView, we do not draw.
+ * This method is called by WebView.onDraw to take care of the part that
+ * needs to be called.
+ */
+ /* package */ void onDrawSubstitute() {
+ if (!willNotDraw()) {
+ // If the WebTextView is set to draw, such as in the case of a
+ // password, onDraw calls updateSelection(), so this code path is
+ // unnecessary.
+ return;
+ }
+ // This code is copied from TextView.onDraw(). That code does not get
+ // executed, however, because the WebTextView does not draw, allowing
+ // webkit's drawing to show through.
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null && imm.isActive(this)) {
+ Spannable sp = (Spannable) getText();
+ int selStart = Selection.getSelectionStart(sp);
+ int selEnd = Selection.getSelectionEnd(sp);
+ int candStart = EditableInputConnection.getComposingSpanStart(sp);
+ int candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+ imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
+ }
+ }
+
@Override
protected void onDraw(Canvas canvas) {
// onDraw should only be called for password fields. If WebTextView is
@@ -360,19 +388,8 @@ import java.util.ArrayList;
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
- if (mInSetTextAndKeepSelection) return;
- // This code is copied from TextView.onDraw(). That code does not get
- // executed, however, because the WebTextView does not draw, allowing
- // webkit's drawing to show through.
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null && imm.isActive(this)) {
- Spannable sp = (Spannable) getText();
- int candStart = EditableInputConnection.getComposingSpanStart(sp);
- int candEnd = EditableInputConnection.getComposingSpanEnd(sp);
- imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
- }
if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType
- && mWebView != null) {
+ && mWebView != null && !mInSetTextAndKeepSelection) {
if (DebugFlags.WEB_TEXT_VIEW) {
Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart
+ " selEnd=" + selEnd);
@@ -481,9 +498,10 @@ import java.util.ArrayList;
// to big for the case of a small textfield.
int smallerSlop = slop/2;
if (dx > smallerSlop || dy > smallerSlop) {
- if (mWebView != null) {
- float maxScrollX = (float) Touch.getMaxScrollX(this,
- getLayout(), mScrollY);
+ Layout layout = getLayout();
+ if (mWebView != null && layout != null) {
+ float maxScrollX = (float) Touch.getMaxScrollX(this, layout,
+ mScrollY);
if (DebugFlags.WEB_TEXT_VIEW) {
Log.v(LOGTAG, "onTouchEvent x=" + mScrollX + " y="
+ mScrollY + " maxX=" + maxScrollX);
@@ -667,6 +685,7 @@ import java.util.ArrayList;
} else {
Selection.setSelection(text, selection, selection);
}
+ if (mWebView != null) mWebView.incrementTextGeneration();
}
/**
@@ -919,14 +938,4 @@ import java.util.ArrayList;
/* package */ void updateCachedTextfield() {
mWebView.updateCachedTextfield(getText().toString());
}
-
- @Override
- public boolean requestRectangleOnScreen(Rect rectangle) {
- // don't scroll while in zoom animation. When it is done, we will adjust
- // the WebTextView if it is in editing mode.
- if (!mWebView.inAnimateZoom()) {
- return super.requestRectangleOnScreen(rectangle);
- }
- return false;
- }
}
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 4ca210f..0c8fc79 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -22,22 +22,20 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.DialogInterface.OnCancelListener;
-import android.content.pm.PackageManager;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.CornerPathEffect;
+import android.graphics.DrawFilter;
import android.graphics.Interpolator;
-import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Picture;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
-import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.http.SslCertificate;
@@ -46,6 +44,7 @@ import android.os.Handler;
import android.os.Message;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.speech.tts.TextToSpeech;
import android.text.IClipboard;
import android.text.Selection;
import android.text.Spannable;
@@ -64,32 +63,29 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
-import android.view.animation.AlphaAnimation;
+import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebTextView.AutoCompleteAdapter;
import android.webkit.WebViewCore.EventHub;
import android.webkit.WebViewCore.TouchEventData;
+import android.webkit.WebViewCore.TouchHighlightData;
import android.widget.AbsoluteLayout;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckedTextView;
-import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Scroller;
import android.widget.Toast;
-import android.widget.ZoomButtonsController;
-import android.widget.ZoomControls;
import android.widget.AdapterView.OnItemClickListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
-import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
@@ -310,49 +306,7 @@ public class WebView extends AbsoluteLayout
static final String LOGTAG = "webview";
- private static class ExtendedZoomControls extends FrameLayout {
- public ExtendedZoomControls(Context context, AttributeSet attrs) {
- super(context, attrs);
- LayoutInflater inflater = (LayoutInflater)
- context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true);
- mPlusMinusZoomControls = (ZoomControls) findViewById(
- com.android.internal.R.id.zoomControls);
- findViewById(com.android.internal.R.id.zoomMagnify).setVisibility(
- View.GONE);
- }
-
- public void show(boolean showZoom, boolean canZoomOut) {
- mPlusMinusZoomControls.setVisibility(
- showZoom ? View.VISIBLE : View.GONE);
- fade(View.VISIBLE, 0.0f, 1.0f);
- }
-
- public void hide() {
- fade(View.GONE, 1.0f, 0.0f);
- }
-
- private void fade(int visibility, float startAlpha, float endAlpha) {
- AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha);
- anim.setDuration(500);
- startAnimation(anim);
- setVisibility(visibility);
- }
-
- public boolean hasFocus() {
- return mPlusMinusZoomControls.hasFocus();
- }
-
- public void setOnZoomInClickListener(OnClickListener listener) {
- mPlusMinusZoomControls.setOnZoomInClickListener(listener);
- }
-
- public void setOnZoomOutClickListener(OnClickListener listener) {
- mPlusMinusZoomControls.setOnZoomOutClickListener(listener);
- }
-
- ZoomControls mPlusMinusZoomControls;
- }
+ private ZoomManager mZoomManager;
/**
* Transportation object for returning WebView across thread boundaries.
@@ -398,6 +352,8 @@ public class WebView extends AbsoluteLayout
// more key events.
private int mTextGeneration;
+ /* package */ void incrementTextGeneration() { mTextGeneration++; }
+
// Used by WebViewCore to create child views.
/* package */ final ViewManager mViewManager;
@@ -445,6 +401,10 @@ public class WebView extends AbsoluteLayout
private float mLastVelX;
private float mLastVelY;
+ // only trigger accelerated fling if the new velocity is at least
+ // MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION times of the previous velocity
+ private static final float MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION = 0.2f;
+
/**
* Touch mode
*/
@@ -456,8 +416,7 @@ public class WebView extends AbsoluteLayout
private static final int TOUCH_SHORTPRESS_MODE = 5;
private static final int TOUCH_DOUBLE_TAP_MODE = 6;
private static final int TOUCH_DONE_MODE = 7;
- private static final int TOUCH_SELECT_MODE = 8;
- private static final int TOUCH_PINCH_DRAG = 9;
+ private static final int TOUCH_PINCH_DRAG = 8;
// Whether to forward the touch events to WebCore
private boolean mForwardTouchEvents = false;
@@ -496,10 +455,6 @@ public class WebView extends AbsoluteLayout
// true if onPause has been called (and not onResume)
private boolean mIsPaused;
- // true if, during a transition to a new page, we're delaying
- // deleting a root layer until there's something to draw of the new page.
- private boolean mDelayedDeleteRootLayer;
-
/**
* Customizable constant
*/
@@ -521,9 +476,6 @@ public class WebView extends AbsoluteLayout
private static final int MIN_FLING_TIME = 250;
// draw unfiltered after drag is held without movement
private static final int MOTIONLESS_TIME = 100;
- // The time that the Zoom Controls are visible before fading away
- private static final long ZOOM_CONTROLS_TIMEOUT =
- ViewConfiguration.getZoomControlsTimeout();
// The amount of content to overlap between two screens when going through
// pages with the space bar, in pixels.
private static final int PAGE_SCROLL_OVERLAP = 24;
@@ -564,15 +516,24 @@ public class WebView extends AbsoluteLayout
private static final int MOTIONLESS_IGNORE = 3;
private int mHeldMotionless;
- // whether support multi-touch
- private boolean mSupportMultiTouch;
- // use the framework's ScaleGestureDetector to handle multi-touch
- private ScaleGestureDetector mScaleDetector;
-
- // the anchor point in the document space where VIEW_SIZE_CHANGED should
- // apply to
- private int mAnchorX;
- private int mAnchorY;
+ // An instance for injecting accessibility in WebViews with disabled
+ // JavaScript or ones for which no accessibility script exists
+ private AccessibilityInjector mAccessibilityInjector;
+
+ // the color used to highlight the touch rectangles
+ private static final int mHightlightColor = 0x33000000;
+ // the round corner for the highlight path
+ private static final float TOUCH_HIGHLIGHT_ARC = 5.0f;
+ // the region indicating where the user touched on the screen
+ private Region mTouchHighlightRegion = new Region();
+ // the paint for the touch highlight
+ private Paint mTouchHightlightPaint;
+ // debug only
+ private static final boolean DEBUG_TOUCH_HIGHLIGHT = true;
+ private static final int TOUCH_HIGHLIGHT_ELAPSE_TIME = 2000;
+ private Paint mTouchCrossHairColor;
+ private int mTouchHighlightX;
+ private int mTouchHighlightY;
/*
* Private message ids
@@ -606,7 +567,7 @@ public class WebView extends AbsoluteLayout
static final int WEBCORE_INITIALIZED_MSG_ID = 107;
static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 108;
static final int UPDATE_ZOOM_RANGE = 109;
- static final int MOVE_OUT_OF_PLUGIN = 110;
+ static final int UNHANDLED_NAV_KEY = 110;
static final int CLEAR_TEXT_ENTRY = 111;
static final int UPDATE_TEXT_SELECTION_MSG_ID = 112;
static final int SHOW_RECT_MSG_ID = 113;
@@ -620,16 +581,19 @@ public class WebView extends AbsoluteLayout
static final int SHOW_FULLSCREEN = 120;
static final int HIDE_FULLSCREEN = 121;
static final int DOM_FOCUS_CHANGED = 122;
- static final int IMMEDIATE_REPAINT_MSG_ID = 123;
- static final int SET_ROOT_LAYER_MSG_ID = 124;
+ static final int REPLACE_BASE_CONTENT = 123;
+ // 124;
static final int RETURN_LABEL = 125;
static final int FIND_AGAIN = 126;
static final int CENTER_FIT_RECT = 127;
static final int REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID = 128;
static final int SET_SCROLLBAR_MODES = 129;
+ static final int SELECTION_STRING_CHANGED = 130;
+ static final int SET_TOUCH_HIGHLIGHT_RECTS = 131;
+ static final int SAVE_WEBARCHIVE_FINISHED = 132;
private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID;
- private static final int LAST_PACKAGE_MSG_ID = SET_SCROLLBAR_MODES;
+ private static final int LAST_PACKAGE_MSG_ID = SET_TOUCH_HIGHLIGHT_RECTS;
static final String[] HandlerPrivateDebugString = {
"REMEMBER_PASSWORD", // = 1;
@@ -654,7 +618,7 @@ public class WebView extends AbsoluteLayout
"WEBCORE_INITIALIZED_MSG_ID", // = 107;
"UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 108;
"UPDATE_ZOOM_RANGE", // = 109;
- "MOVE_OUT_OF_PLUGIN", // = 110;
+ "UNHANDLED_NAV_KEY", // = 110;
"CLEAR_TEXT_ENTRY", // = 111;
"UPDATE_TEXT_SELECTION_MSG_ID", // = 112;
"SHOW_RECT_MSG_ID", // = 113;
@@ -667,13 +631,16 @@ public class WebView extends AbsoluteLayout
"SHOW_FULLSCREEN", // = 120;
"HIDE_FULLSCREEN", // = 121;
"DOM_FOCUS_CHANGED", // = 122;
- "IMMEDIATE_REPAINT_MSG_ID", // = 123;
- "SET_ROOT_LAYER_MSG_ID", // = 124;
+ "REPLACE_BASE_CONTENT", // = 123;
+ "124", // = 124;
"RETURN_LABEL", // = 125;
"FIND_AGAIN", // = 126;
"CENTER_FIT_RECT", // = 127;
"REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID", // = 128;
- "SET_SCROLLBAR_MODES" // = 129;
+ "SET_SCROLLBAR_MODES", // = 129;
+ "SELECTION_STRING_CHANGED", // = 130;
+ "SET_TOUCH_HIGHLIGHT_RECTS", // = 131;
+ "SAVE_WEBARCHIVE_FINISHED" // = 132;
};
// If the site doesn't use the viewport meta tag to specify the viewport,
@@ -686,49 +653,9 @@ public class WebView extends AbsoluteLayout
// the minimum preferred width is huge, an upper limit is needed.
static int sMaxViewportWidth = DEFAULT_VIEWPORT_WIDTH;
- // 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;
- private float mMinZoomScale;
- private boolean mMinZoomScaleFixed = true;
-
// initial scale in percent. 0 means using default.
private int mInitialScaleInPercent = 0;
- // while in the zoom overview mode, the page's width is fully fit to the
- // current window. The page is alive, in another words, you can click to
- // follow the links. Double tap will toggle between zoom overview mode and
- // the last zoom scale.
- boolean mInZoomOverview = false;
-
- // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn,
- // engadget always have wider mContentWidth no matter what viewport size is.
- int mZoomOverviewWidth = DEFAULT_VIEWPORT_WIDTH;
- float mTextWrapScale;
-
- // default scale. Depending on the display density.
- static int DEFAULT_SCALE_PERCENT;
- private float mDefaultScale;
-
- private static float MINIMUM_SCALE_INCREMENT = 0.01f;
-
- // set to true temporarily during ScaleGesture triggered zoom
- private boolean mPreviewZoomOnly = false;
-
- // computed scale and inverse, from mZoomWidth.
- 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;
- private float mInvFinalZoomScale;
- private int mInitialScrollX;
- private int mInitialScrollY;
- private long mZoomStart;
- private static final int ZOOM_ANIMATION_LENGTH = 500;
-
private boolean mUserScroll = false;
private int mSnapScrollMode = SNAP_NONE;
@@ -752,6 +679,19 @@ public class WebView extends AbsoluteLayout
private int mHorizontalScrollBarMode = SCROLLBAR_AUTO;
private int mVerticalScrollBarMode = SCROLLBAR_AUTO;
+ // the alias via which accessibility JavaScript interface is exposed
+ private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility";
+
+ // JavaScript to inject the script chooser which will
+ // pick the right script for the current URL
+ private static final String ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT =
+ "javascript:(function() {" +
+ " var chooser = document.createElement('script');" +
+ " chooser.type = 'text/javascript';" +
+ " chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" +
+ " document.getElementsByTagName('head')[0].appendChild(chooser);" +
+ " })();";
+
// Used to match key downs and key ups
private boolean mGotKeyDown;
@@ -856,43 +796,6 @@ public class WebView extends AbsoluteLayout
}
}
- // The View containing the zoom controls
- private ExtendedZoomControls mZoomControls;
- private Runnable mZoomControlRunnable;
-
- // mZoomButtonsController will be lazy initialized in
- // getZoomButtonsController() to get better performance.
- private ZoomButtonsController mZoomButtonsController;
-
- // These keep track of the center point of the zoom. They are used to
- // determine the point around which we should zoom.
- private float mZoomCenterX;
- private float mZoomCenterY;
-
- private ZoomButtonsController.OnZoomListener mZoomListener =
- new ZoomButtonsController.OnZoomListener() {
-
- public void onVisibilityChanged(boolean visible) {
- if (visible) {
- switchOutDrawHistory();
- // Bring back the hidden zoom controls.
- mZoomButtonsController.getZoomControls().setVisibility(
- View.VISIBLE);
- updateZoomButtonsEnabled();
- }
- }
-
- public void onZoom(boolean zoomIn) {
- if (zoomIn) {
- zoomIn();
- } else {
- zoomOut();
- }
-
- updateZoomButtonsEnabled();
- }
- };
-
/**
* Construct a new WebView with a Context object.
* @param context A Context object used to access application assets.
@@ -928,51 +831,37 @@ public class WebView extends AbsoluteLayout
* @param context A Context object used to access application assets.
* @param attrs An AttributeSet passed to our parent.
* @param defStyle The default style resource ID.
- * @param javascriptInterfaces is a Map of intareface names, as keys, and
+ * @param javascriptInterfaces is a Map of interface names, as keys, and
* object implementing those interfaces, as values.
* @hide pending API council approval.
*/
protected WebView(Context context, AttributeSet attrs, int defStyle,
Map<String, Object> javascriptInterfaces) {
super(context, attrs, defStyle);
- init();
+
+ if (AccessibilityManager.getInstance(context).isEnabled()) {
+ if (javascriptInterfaces == null) {
+ javascriptInterfaces = new HashMap<String, Object>();
+ }
+ exposeAccessibilityJavaScriptApi(javascriptInterfaces);
+ }
mCallbackProxy = new CallbackProxy(context, this);
mViewManager = new ViewManager(this);
mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces);
mDatabase = WebViewDatabase.getInstance(context);
mScroller = new Scroller(context);
+ mZoomManager = new ZoomManager(this, mCallbackProxy);
+ /* The init method must follow the creation of certain member variables,
+ * such as the mZoomManager.
+ */
+ init();
updateMultiTouchSupport(context);
}
void updateMultiTouchSupport(Context context) {
- WebSettings settings = getSettings();
- mSupportMultiTouch = context.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
- && settings.supportZoom() && settings.getBuiltInZoomControls();
- if (mSupportMultiTouch && (mScaleDetector == null)) {
- mScaleDetector = new ScaleGestureDetector(context,
- new ScaleDetectorListener());
- } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
- mScaleDetector = null;
- }
- }
-
- private void updateZoomButtonsEnabled() {
- if (mZoomButtonsController == null) return;
- boolean canZoomIn = mActualScale < mMaxZoomScale;
- boolean canZoomOut = mActualScale > mMinZoomScale && !mInZoomOverview;
- if (!canZoomIn && !canZoomOut) {
- // Hide the zoom in and out buttons, as well as the fit to page
- // button, if the page cannot zoom
- mZoomButtonsController.getZoomControls().setVisibility(View.GONE);
- } else {
- // Set each one individually, as a page may be able to zoom in
- // or out.
- mZoomButtonsController.setZoomInEnabled(canZoomIn);
- mZoomButtonsController.setZoomOutEnabled(canZoomOut);
- }
+ mZoomManager.updateMultiTouchSupport(context);
}
private void init() {
@@ -992,34 +881,35 @@ public class WebView extends AbsoluteLayout
// 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 * density);
- // density adjusted scale factors
- DEFAULT_SCALE_PERCENT = (int) (100 * density);
- mDefaultScale = density;
- mActualScale = density;
- mInvActualScale = 1 / density;
- mTextWrapScale = 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;
+ mZoomManager.init(density);
mMaximumFling = configuration.getScaledMaximumFlingVelocity();
}
+ /**
+ * Exposes accessibility APIs to JavaScript by appending them to the JavaScript
+ * interfaces map provided by the WebView client. In case of conflicting
+ * alias with the one of the accessibility API the user specified one wins.
+ *
+ * @param javascriptInterfaces A map with interfaces to be exposed to JavaScript.
+ */
+ private void exposeAccessibilityJavaScriptApi(Map<String, Object> javascriptInterfaces) {
+ if (javascriptInterfaces.containsKey(ALIAS_ACCESSIBILITY_JS_INTERFACE)) {
+ Log.w(LOGTAG, "JavaScript interface mapped to \"" + ALIAS_ACCESSIBILITY_JS_INTERFACE
+ + "\" overrides the accessibility API JavaScript interface. No accessibility"
+ + "API will be exposed to JavaScript!");
+ return;
+ }
+
+ // expose the TTS for now ...
+ javascriptInterfaces.put(ALIAS_ACCESSIBILITY_JS_INTERFACE,
+ new TextToSpeech(getContext(), null));
+ }
+
/* package */void updateDefaultZoomDensity(int zoomDensity) {
- final float density = getContext().getResources().getDisplayMetrics().density
+ final float density = mContext.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, true, false);
- }
+ mNavSlop = (int) (16 * density);
+ mZoomManager.updateDefaultZoomDensity(density);
}
/* package */ boolean onSavePassword(String schemePlusHost, String username,
@@ -1136,14 +1026,14 @@ public class WebView extends AbsoluteLayout
* returns the height of the titlebarview (if any). Does not care about
* scrolling
*/
- private int getTitleHeight() {
+ int getTitleHeight() {
return mTitleBar != null ? mTitleBar.getHeight() : 0;
}
/*
* Return the amount of the titlebarview (if any) that is visible
*/
- private int getVisibleTitleHeight() {
+ int getVisibleTitleHeight() {
return Math.max(getTitleHeight() - mScrollY, 0);
}
@@ -1404,29 +1294,23 @@ public class WebView extends AbsoluteLayout
// now update the bundle
b.putInt("scrollX", mScrollX);
b.putInt("scrollY", mScrollY);
- b.putFloat("scale", mActualScale);
- b.putFloat("textwrapScale", mTextWrapScale);
- b.putBoolean("overview", mInZoomOverview);
+ mZoomManager.saveZoomState(b);
return true;
}
private void restoreHistoryPictureFields(Picture p, Bundle b) {
int sx = b.getInt("scrollX", 0);
int sy = b.getInt("scrollY", 0);
- float scale = b.getFloat("scale", 1.0f);
+
mDrawHistory = true;
mHistoryPicture = p;
mScrollX = sx;
mScrollY = sy;
+ mZoomManager.restoreZoomState(b);
+ final float scale = mZoomManager.getScale();
mHistoryWidth = Math.round(p.getWidth() * scale);
mHistoryHeight = Math.round(p.getHeight() * scale);
- // as getWidth() / getHeight() of the view are not available yet, set up
- // mActualScale, so that when onSizeChanged() is called, the rest will
- // be set correctly
- mActualScale = scale;
- mInvActualScale = 1 / scale;
- mTextWrapScale = b.getFloat("textwrapScale", scale);
- mInZoomOverview = b.getBoolean("overview");
+
invalidate();
}
@@ -1638,6 +1522,45 @@ public class WebView extends AbsoluteLayout
}
/**
+ * Saves the current view as a web archive.
+ *
+ * @param filename The filename where the archive should be placed.
+ */
+ public void saveWebArchive(String filename) {
+ saveWebArchive(filename, false, null);
+ }
+
+ /* package */ static class SaveWebArchiveMessage {
+ SaveWebArchiveMessage (String basename, boolean autoname, ValueCallback<String> callback) {
+ mBasename = basename;
+ mAutoname = autoname;
+ mCallback = callback;
+ }
+
+ /* package */ final String mBasename;
+ /* package */ final boolean mAutoname;
+ /* package */ final ValueCallback<String> mCallback;
+ /* package */ String mResultFile;
+ }
+
+ /**
+ * Saves the current view as a web archive.
+ *
+ * @param basename The filename where the archive should be placed.
+ * @param autoname If false, takes basename to be a file. If true, basename
+ * is assumed to be a directory in which a filename will be
+ * chosen according to the url of the current page.
+ * @param callback Called after the web archive has been saved. The
+ * parameter for onReceiveValue will either be the filename
+ * under which the file was saved, or null if saving the
+ * file failed.
+ */
+ public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) {
+ mWebViewCore.sendMessage(EventHub.SAVE_WEBARCHIVE,
+ new SaveWebArchiveMessage(basename, autoname, callback));
+ }
+
+ /**
* Stop the current load.
*/
public void stopLoading() {
@@ -1806,6 +1729,7 @@ public class WebView extends AbsoluteLayout
public void clearView() {
mContentWidth = 0;
mContentHeight = 0;
+ nativeSetBaseLayer(0);
mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT);
}
@@ -1819,8 +1743,9 @@ public class WebView extends AbsoluteLayout
* bounds of the view.
*/
public Picture capturePicture() {
- if (null == mWebViewCore) return null; // check for out of memory tab
- return mWebViewCore.copyContentPicture();
+ Picture result = new Picture();
+ nativeCopyBaseContentToPicture(result);
+ return result;
}
/**
@@ -1849,7 +1774,7 @@ public class WebView extends AbsoluteLayout
* @return The current scale.
*/
public float getScale() {
- return mActualScale;
+ return mZoomManager.getScale();
}
/**
@@ -1861,7 +1786,7 @@ public class WebView extends AbsoluteLayout
* @param scaleInPercent The initial scale in percent.
*/
public void setInitialScale(int scaleInPercent) {
- mInitialScaleInPercent = scaleInPercent;
+ mZoomManager.setInitialScaleInPercent(scaleInPercent);
}
/**
@@ -1875,13 +1800,7 @@ public class WebView extends AbsoluteLayout
return;
}
clearTextEntry(false);
- if (getSettings().getBuiltInZoomControls()) {
- getZoomButtonsController().setVisible(true);
- } else {
- mPrivateHandler.removeCallbacks(mZoomControlRunnable);
- mPrivateHandler.postDelayed(mZoomControlRunnable,
- ZOOM_CONTROLS_TIMEOUT);
- }
+ mZoomManager.invokeZoomPicker();
}
/**
@@ -1996,7 +1915,7 @@ public class WebView extends AbsoluteLayout
msg.sendToTarget();
}
- private static int pinLoc(int x, int viewMax, int docMax) {
+ static int pinLoc(int x, int viewMax, int docMax) {
// Log.d(LOGTAG, "-- pinLoc " + x + " " + viewMax + " " + docMax);
if (docMax < viewMax) { // the doc has room on the sides for "blank"
// pin the short document to the top/left of the screen
@@ -2013,12 +1932,12 @@ public class WebView extends AbsoluteLayout
}
// Expects x in view coordinates
- private int pinLocX(int x) {
+ int pinLocX(int x) {
return pinLoc(x, getViewWidth(), computeHorizontalScrollRange());
}
// Expects y in view coordinates
- private int pinLocY(int y) {
+ int pinLocY(int y) {
return pinLoc(y, getViewHeightWithTitle(),
computeVerticalScrollRange() + getTitleHeight());
}
@@ -2068,7 +1987,7 @@ public class WebView extends AbsoluteLayout
* height.
*/
private int viewToContentDimension(int d) {
- return Math.round(d * mInvActualScale);
+ return Math.round(d * mZoomManager.getInvScale());
}
/**
@@ -2094,7 +2013,7 @@ public class WebView extends AbsoluteLayout
* Returns the result as a float.
*/
private float viewToContentXf(int x) {
- return x * mInvActualScale;
+ return x * mZoomManager.getInvScale();
}
/**
@@ -2103,7 +2022,7 @@ public class WebView extends AbsoluteLayout
* embedded into the WebView. Returns the result as a float.
*/
private float viewToContentYf(int y) {
- return (y - getTitleHeight()) * mInvActualScale;
+ return (y - getTitleHeight()) * mZoomManager.getInvScale();
}
/**
@@ -2113,7 +2032,7 @@ public class WebView extends AbsoluteLayout
* height.
*/
/*package*/ int contentToViewDimension(int d) {
- return Math.round(d * mActualScale);
+ return Math.round(d * mZoomManager.getScale());
}
/**
@@ -2154,7 +2073,7 @@ public class WebView extends AbsoluteLayout
// Called by JNI to invalidate the View, given rectangle coordinates in
// content space
private void viewInvalidate(int l, int t, int r, int b) {
- final float scale = mActualScale;
+ final float scale = mZoomManager.getScale();
final int dy = getTitleHeight();
invalidate((int)Math.floor(l * scale),
(int)Math.floor(t * scale) + dy,
@@ -2165,7 +2084,7 @@ public class WebView extends AbsoluteLayout
// Called by JNI to invalidate the View after a delay, given rectangle
// coordinates in content space
private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) {
- final float scale = mActualScale;
+ final float scale = mZoomManager.getScale();
final int dy = getTitleHeight();
postInvalidateDelayed(delay,
(int)Math.floor(l * scale),
@@ -2203,13 +2122,7 @@ public class WebView extends AbsoluteLayout
// updated when we get out of that mode.
if (!mDrawHistory) {
// repin our scroll, taking into account the new content size
- int oldX = mScrollX;
- int oldY = mScrollY;
- mScrollX = pinLocX(mScrollX);
- mScrollY = pinLocY(mScrollY);
- if (oldX != mScrollX || oldY != mScrollY) {
- onScrollChanged(mScrollX, mScrollY, oldX, oldY);
- }
+ updateScrollCoordinates(pinLocX(mScrollX), pinLocY(mScrollY));
if (!mScroller.isFinished()) {
// We are in the middle of a scroll. Repin the final scroll
// position.
@@ -2221,77 +2134,12 @@ public class WebView extends AbsoluteLayout
contentSizeChanged(updateLayout);
}
- private void setNewZoomScale(float scale, boolean updateTextWrapScale,
- boolean force) {
- if (scale < mMinZoomScale) {
- scale = mMinZoomScale;
- // set mInZoomOverview for non mobile sites
- if (scale < mDefaultScale) mInZoomOverview = true;
- } else if (scale > mMaxZoomScale) {
- scale = mMaxZoomScale;
- }
- if (updateTextWrapScale) {
- mTextWrapScale = scale;
- // reset mLastHeightSent to force VIEW_SIZE_CHANGED sent to WebKit
- mLastHeightSent = 0;
- }
- if (scale != mActualScale || force) {
- if (mDrawHistory) {
- // If history Picture is drawn, don't update scroll. They will
- // be updated when we get out of that mode.
- if (scale != mActualScale && !mPreviewZoomOnly) {
- mCallbackProxy.onScaleChanged(mActualScale, scale);
- }
- mActualScale = scale;
- mInvActualScale = 1 / scale;
- sendViewSizeZoom();
- } else {
- // update our scroll so we don't appear to jump
- // i.e. keep the center of the doc in the center of the view
-
- int oldX = mScrollX;
- int oldY = mScrollY;
- float ratio = scale * mInvActualScale; // old inverse
- float sx = ratio * oldX + (ratio - 1) * mZoomCenterX;
- float sy = ratio * oldY + (ratio - 1)
- * (mZoomCenterY - getTitleHeight());
-
- // now update our new scale and inverse
- if (scale != mActualScale && !mPreviewZoomOnly) {
- mCallbackProxy.onScaleChanged(mActualScale, scale);
- }
- mActualScale = scale;
- mInvActualScale = 1 / scale;
-
- // Scale all the child views
- mViewManager.scaleAll();
-
- // as we don't have animation for scaling, don't do animation
- // for scrolling, as it causes weird intermediate state
- // pinScrollTo(Math.round(sx), Math.round(sy));
- mScrollX = pinLocX(Math.round(sx));
- mScrollY = pinLocY(Math.round(sy));
-
- // update webkit
- if (oldX != mScrollX || oldY != mScrollY) {
- onScrollChanged(mScrollX, mScrollY, oldX, oldY);
- } else {
- // the scroll position is adjusted at the beginning of the
- // zoom animation. But we want to update the WebKit at the
- // end of the zoom animation. See comments in onScaleEnd().
- sendOurVisibleRect();
- }
- sendViewSizeZoom();
- }
- }
- }
-
// Used to avoid sending many visible rect messages.
private Rect mLastVisibleRectSent;
private Rect mLastGlobalRect;
- private Rect sendOurVisibleRect() {
- if (mPreviewZoomOnly) return mLastVisibleRectSent;
+ Rect sendOurVisibleRect() {
+ if (mZoomManager.isPreventingWebkitUpdates()) return mLastVisibleRectSent;
Rect rect = new Rect();
calcOurContentVisibleRect(rect);
@@ -2324,9 +2172,6 @@ public class WebView extends AbsoluteLayout
Point p = new Point();
getGlobalVisibleRect(r, p);
r.offset(-p.x, -p.y);
- if (mFindIsUp) {
- r.bottom -= mFindHeight;
- }
}
// Sets r to be our visible rectangle in content coordinates
@@ -2373,16 +2218,19 @@ public class WebView extends AbsoluteLayout
/**
* Compute unzoomed width and height, and if they differ from the last
- * values we sent, send them to webkit (to be used has new viewport)
+ * values we sent, send them to webkit (to be used as new viewport)
+ *
+ * @param force ensures that the message is sent to webkit even if the width
+ * or height has not changed since the last message
*
* @return true if new values were sent
*/
- private boolean sendViewSizeZoom() {
- if (mPreviewZoomOnly) return false;
+ boolean sendViewSizeZoom(boolean force) {
+ if (mZoomManager.isPreventingWebkitUpdates()) return false;
int viewWidth = getViewWidth();
- int newWidth = Math.round(viewWidth * mInvActualScale);
- int newHeight = Math.round(getViewHeight() * mInvActualScale);
+ int newWidth = Math.round(viewWidth * mZoomManager.getInvScale());
+ int newHeight = Math.round(getViewHeight() * mZoomManager.getInvScale());
/*
* Because the native side may have already done a layout before the
* View system was able to measure us, we have to send a height of 0 to
@@ -2395,19 +2243,20 @@ public class WebView extends AbsoluteLayout
newHeight = 0;
}
// Avoid sending another message if the dimensions have not changed.
- if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) {
+ if (newWidth != mLastWidthSent || newHeight != mLastHeightSent || force) {
ViewSizeData data = new ViewSizeData();
data.mWidth = newWidth;
data.mHeight = newHeight;
- data.mTextWrapWidth = Math.round(viewWidth / mTextWrapScale);;
- data.mScale = mActualScale;
- data.mIgnoreHeight = mZoomScale != 0 && !mHeightCanMeasure;
- data.mAnchorX = mAnchorX;
- data.mAnchorY = mAnchorY;
+ data.mTextWrapWidth = Math.round(viewWidth / mZoomManager.getTextWrapScale());
+ data.mScale = mZoomManager.getScale();
+ data.mIgnoreHeight = mZoomManager.isFixedLengthAnimationInProgress()
+ && !mHeightCanMeasure;
+ data.mAnchorX = mZoomManager.getDocumentAnchorX();
+ data.mAnchorY = mZoomManager.getDocumentAnchorY();
mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data);
mLastWidthSent = newWidth;
mLastHeightSent = newHeight;
- mAnchorX = mAnchorY = 0;
+ mZoomManager.clearDocumentAnchor();
return true;
}
return false;
@@ -2418,12 +2267,12 @@ public class WebView extends AbsoluteLayout
if (mDrawHistory) {
return mHistoryWidth;
} else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF
- && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) {
+ && !mZoomManager.canZoomOut()) {
// only honor the scrollbar mode when it is at minimum zoom level
return computeHorizontalScrollExtent();
} else {
// to avoid rounding error caused unnecessary scrollbar, use floor
- return (int) Math.floor(mContentWidth * mActualScale);
+ return (int) Math.floor(mContentWidth * mZoomManager.getScale());
}
}
@@ -2432,12 +2281,12 @@ public class WebView extends AbsoluteLayout
if (mDrawHistory) {
return mHistoryHeight;
} else if (mVerticalScrollBarMode == SCROLLBAR_ALWAYSOFF
- && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) {
+ && !mZoomManager.canZoomOut()) {
// only honor the scrollbar mode when it is at minimum zoom level
return computeVerticalScrollExtent();
} else {
// to avoid rounding error caused unnecessary scrollbar, use floor
- return (int) Math.floor(mContentHeight * mActualScale);
+ return (int) Math.floor(mContentHeight * mZoomManager.getScale());
}
}
@@ -2505,7 +2354,9 @@ public class WebView extends AbsoluteLayout
}
/**
- * Get the touch icon url for the apple-touch-icon <link> element.
+ * Get the touch icon url for the apple-touch-icon <link> element, or
+ * a URL on this site's server pointing to the standard location of a
+ * touch icon.
* @hide
*/
public String getTouchIconUrl() {
@@ -2682,19 +2533,22 @@ public class WebView extends AbsoluteLayout
*/
public void setFindIsUp(boolean isUp) {
mFindIsUp = isUp;
- if (isUp) {
- recordNewContentSize(mContentWidth, mContentHeight + mFindHeight,
- false);
- }
if (0 == mNativeClass) return; // client isn't initialized
nativeSetFindIsUp(isUp);
}
+ /**
+ * @hide
+ */
+ public int findIndex() {
+ if (0 == mNativeClass) return -1;
+ return nativeFindIndex();
+ }
+
// Used to know whether the find dialog is open. Affects whether
// or not we draw the highlights for matches.
private boolean mFindIsUp;
- private int mFindHeight;
// Keep track of the last string sent, so we can search again after an
// orientation change or the dismissal of the soft keyboard.
private String mLastFind;
@@ -2769,8 +2623,6 @@ public class WebView extends AbsoluteLayout
}
clearMatches();
setFindIsUp(false);
- recordNewContentSize(mContentWidth, mContentHeight - mFindHeight,
- false);
// Now that the dialog has been removed, ensure that we scroll to a
// location that is not beyond the end of the page.
pinScrollTo(mScrollX, mScrollY, false, 0);
@@ -2778,16 +2630,6 @@ public class WebView extends AbsoluteLayout
}
/**
- * @hide
- */
- public void setFindDialogHeight(int height) {
- if (DebugFlags.WEB_VIEW) {
- Log.v(LOGTAG, "setFindDialogHeight height=" + height);
- }
- mFindHeight = height;
- }
-
- /**
* Query the document to see if it contains any image references. The
* message object will be dispatched with arg1 being set to 1 if images
* were found and 0 if the document does not reference any images.
@@ -2810,6 +2652,11 @@ public class WebView extends AbsoluteLayout
postInvalidate(); // So we draw again
if (oldX != mScrollX || oldY != mScrollY) {
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ } else {
+ abortAnimation();
+ mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY);
+ WebViewCore.resumePriority();
+ WebViewCore.resumeUpdatePicture(mWebViewCore);
}
} else {
super.computeScroll();
@@ -2898,6 +2745,29 @@ public class WebView extends AbsoluteLayout
}
mPageThatNeedsToSlideTitleBarOffScreen = null;
}
+
+ injectAccessibilityForUrl(url);
+ }
+
+ /**
+ * This method injects accessibility in the loaded document if accessibility
+ * is enabled. If JavaScript is enabled we try to inject a URL specific script.
+ * If no URL specific script is found or JavaScript is disabled we fallback to
+ * the default {@link AccessibilityInjector} implementation.
+ *
+ * @param url The URL loaded by this {@link WebView}.
+ */
+ private void injectAccessibilityForUrl(String url) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ if (getSettings().getJavaScriptEnabled()) {
+ loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT);
+ } else if (mAccessibilityInjector == null) {
+ mAccessibilityInjector = new AccessibilityInjector(this);
+ }
+ } else {
+ // it is possible that accessibility was turned off between reloads
+ mAccessibilityInjector = null;
+ }
}
/**
@@ -3014,7 +2884,7 @@ public class WebView extends AbsoluteLayout
} else {
// If we don't request a layout, try to send our view size to the
// native side to ensure that WebCore has the correct dimensions.
- sendViewSizeZoom();
+ sendViewSizeZoom(false);
}
}
@@ -3144,7 +3014,7 @@ public class WebView extends AbsoluteLayout
* settings.
*/
public WebSettings getSettings() {
- return mWebViewCore.getSettings();
+ return (mWebViewCore != null) ? mWebViewCore.getSettings() : null;
}
/**
@@ -3285,7 +3155,47 @@ public class WebView extends AbsoluteLayout
if (AUTO_REDRAW_HACK && mAutoRedraw) {
invalidate();
}
+ if (inEditingMode()) mWebTextView.onDrawSubstitute();
mWebViewCore.signalRepaintDone();
+
+ // paint the highlight in the end
+ if (!mTouchHighlightRegion.isEmpty()) {
+ if (mTouchHightlightPaint == null) {
+ mTouchHightlightPaint = new Paint();
+ mTouchHightlightPaint.setColor(mHightlightColor);
+ mTouchHightlightPaint.setAntiAlias(true);
+ mTouchHightlightPaint.setPathEffect(new CornerPathEffect(
+ TOUCH_HIGHLIGHT_ARC));
+ }
+ canvas.drawPath(mTouchHighlightRegion.getBoundaryPath(),
+ mTouchHightlightPaint);
+ }
+ if (DEBUG_TOUCH_HIGHLIGHT) {
+ if (getSettings().getNavDump()) {
+ if ((mTouchHighlightX | mTouchHighlightY) != 0) {
+ if (mTouchCrossHairColor == null) {
+ mTouchCrossHairColor = new Paint();
+ mTouchCrossHairColor.setColor(Color.RED);
+ }
+ canvas.drawLine(mTouchHighlightX - mNavSlop,
+ mTouchHighlightY - mNavSlop, mTouchHighlightX
+ + mNavSlop + 1, mTouchHighlightY + mNavSlop
+ + 1, mTouchCrossHairColor);
+ canvas.drawLine(mTouchHighlightX + mNavSlop + 1,
+ mTouchHighlightY - mNavSlop, mTouchHighlightX
+ - mNavSlop,
+ mTouchHighlightY + mNavSlop + 1,
+ mTouchCrossHairColor);
+ }
+ }
+ }
+ }
+
+ private void removeTouchHighlight(boolean removePendingMessage) {
+ if (removePendingMessage) {
+ mWebViewCore.removeMessages(EventHub.GET_TOUCH_HIGHLIGHT_RECTS);
+ }
+ mWebViewCore.sendMessage(EventHub.REMOVE_TOUCH_HIGHLIGHT_RECTS);
}
@Override
@@ -3306,27 +3216,35 @@ public class WebView extends AbsoluteLayout
// Send the click so that the textfield is in focus
centerKeyPressOnTextField();
rebuildWebTextView();
+ } else {
+ clearTextEntry(true);
}
if (inEditingMode()) {
return mWebTextView.performLongClick();
- } else {
- return super.performLongClick();
}
+ /* if long click brings up a context menu, the super function
+ * returns true and we're done. Otherwise, nothing happened when
+ * the user clicked. */
+ if (super.performLongClick()) {
+ return true;
+ }
+ /* In the case where the application hasn't already handled the long
+ * click action, look for a word under the click. If one is found,
+ * animate the text selection into view.
+ * FIXME: no animation code yet */
+ if (mSelectingText) return false; // long click does nothing on selection
+ int x = viewToContentX((int) mLastTouchX + mScrollX);
+ int y = viewToContentY((int) mLastTouchY + mScrollY);
+ setUpSelect();
+ if (mNativeClass != 0 && nativeWordSelection(x, y)) {
+ nativeSetExtendSelection();
+ getWebChromeClient().onSelectionStart();
+ return true;
+ }
+ notifySelectDialogDismissed();
+ return false;
}
- boolean inAnimateZoom() {
- return mZoomScale != 0;
- }
-
- /**
- * Need to adjust the WebTextView after a change in zoom, since mActualScale
- * has changed. This is especially important for password fields, which are
- * drawn by the WebTextView, since it conveys more information than what
- * webkit draws. Thus we need to reposition it to show in the correct
- * place.
- */
- private boolean mNeedToAdjustWebTextView;
-
private boolean didUpdateTextViewBounds(boolean allowIntersect) {
Rect contentBounds = nativeFocusCandidateNodeBounds();
Rect vBox = contentToViewRect(contentBounds);
@@ -3354,25 +3272,54 @@ public class WebView extends AbsoluteLayout
}
}
- private void drawExtras(Canvas canvas, int extras, boolean animationsRunning) {
- // If mNativeClass is 0, we should not reach here, so we do not
- // need to check it again.
- if (animationsRunning) {
- canvas.setDrawFilter(mWebViewCore.mZoomFilter);
+ private void onZoomAnimationStart() {
+ // If it is in password mode, turn it off so it does not draw misplaced.
+ if (inEditingMode() && nativeFocusCandidateIsPassword()) {
+ mWebTextView.setInPassword(false);
}
- nativeDrawExtras(canvas, extras);
- canvas.setDrawFilter(null);
}
+ private void onZoomAnimationEnd() {
+ // adjust the edit text view if needed
+ if (inEditingMode() && didUpdateTextViewBounds(false) && nativeFocusCandidateIsPassword()) {
+ // If it is a password field, start drawing the WebTextView once
+ // again.
+ mWebTextView.setInPassword(true);
+ }
+ }
+
+ void onFixedLengthZoomAnimationStart() {
+ WebViewCore.pauseUpdatePicture(getWebViewCore());
+ onZoomAnimationStart();
+ }
+
+ void onFixedLengthZoomAnimationEnd() {
+ WebViewCore.resumeUpdatePicture(mWebViewCore);
+ onZoomAnimationEnd();
+ }
+
+ private static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG |
+ Paint.DITHER_FLAG |
+ Paint.SUBPIXEL_TEXT_FLAG;
+ private static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG |
+ Paint.DITHER_FLAG;
+
+ private final DrawFilter mZoomFilter =
+ new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG);
+ // If we need to trade better quality for speed, set mScrollFilter to null
+ private final DrawFilter mScrollFilter =
+ new PaintFlagsDrawFilter(SCROLL_BITS, 0);
+
private void drawCoreAndCursorRing(Canvas canvas, int color,
boolean drawCursorRing) {
if (mDrawHistory) {
- canvas.scale(mActualScale, mActualScale);
+ canvas.scale(mZoomManager.getScale(), mZoomManager.getScale());
canvas.drawPicture(mHistoryPicture);
return;
}
+ if (mNativeClass == 0) return;
- boolean animateZoom = mZoomScale != 0;
+ boolean animateZoom = mZoomManager.isFixedLengthAnimationInProgress();
boolean animateScroll = ((!mScroller.isFinished()
|| mVelocityTracker != null)
&& (mTouchMode != TOUCH_DRAG_MODE ||
@@ -3391,59 +3338,9 @@ public class WebView extends AbsoluteLayout
}
}
if (animateZoom) {
- float zoomScale;
- int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
- if (interval < ZOOM_ANIMATION_LENGTH) {
- float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
- zoomScale = 1.0f / (mInvInitialZoomScale
- + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
- invalidate();
- } else {
- zoomScale = mZoomScale;
- // set mZoomScale to be 0 as we have done animation
- mZoomScale = 0;
- WebViewCore.resumeUpdatePicture(mWebViewCore);
- // call invalidate() again to draw with the final filters
- invalidate();
- if (mNeedToAdjustWebTextView) {
- mNeedToAdjustWebTextView = false;
- if (didUpdateTextViewBounds(false)
- && nativeFocusCandidateIsPassword()) {
- // If it is a password field, start drawing the
- // WebTextView once again.
- mWebTextView.setInPassword(true);
- }
- }
- }
- // calculate the intermediate scroll position. As we need to use
- // zoomScale, we can't use pinLocX/Y directly. Copy the logic here.
- float scale = zoomScale * mInvInitialZoomScale;
- int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX)
- - mZoomCenterX);
- tx = -pinLoc(tx, getViewWidth(), Math.round(mContentWidth
- * zoomScale)) + mScrollX;
- int titleHeight = getTitleHeight();
- int ty = Math.round(scale
- * (mInitialScrollY + mZoomCenterY - titleHeight)
- - (mZoomCenterY - titleHeight));
- ty = -(ty <= titleHeight ? Math.max(ty, 0) : pinLoc(ty
- - titleHeight, getViewHeight(), Math.round(mContentHeight
- * zoomScale)) + titleHeight) + mScrollY;
- canvas.translate(tx, ty);
- canvas.scale(zoomScale, zoomScale);
- if (inEditingMode() && !mNeedToAdjustWebTextView
- && mZoomScale != 0) {
- // The WebTextView is up. Keep track of this so we can adjust
- // its size and placement when we finish zooming
- mNeedToAdjustWebTextView = true;
- // If it is in password mode, turn it off so it does not draw
- // misplaced.
- if (nativeFocusCandidateIsPassword()) {
- mWebTextView.setInPassword(false);
- }
- }
+ mZoomManager.animateZoom(canvas);
} else {
- canvas.scale(mActualScale, mActualScale);
+ canvas.scale(mZoomManager.getScale(), mZoomManager.getScale());
}
boolean UIAnimationsRunning = false;
@@ -3455,39 +3352,42 @@ public class WebView extends AbsoluteLayout
// we ask for a repaint.
invalidate();
}
- mWebViewCore.drawContentPicture(canvas, color,
- (animateZoom || mPreviewZoomOnly || UIAnimationsRunning),
- animateScroll);
- if (mNativeClass == 0) return;
+
// decide which adornments to draw
int extras = DRAW_EXTRAS_NONE;
+ if (DebugFlags.WEB_VIEW) {
+ Log.v(LOGTAG, "mFindIsUp=" + mFindIsUp
+ + " mSelectingText=" + mSelectingText
+ + " nativePageShouldHandleShiftAndArrows()="
+ + nativePageShouldHandleShiftAndArrows()
+ + " animateZoom=" + animateZoom);
+ }
if (mFindIsUp) {
- // When the FindDialog is up, only draw the matches if we are not in
- // the process of scrolling them into view.
- if (!animateScroll) {
- extras = DRAW_EXTRAS_FIND;
- }
- } else if (mShiftIsPressed && !nativeFocusIsPlugin()) {
- if (!animateZoom && !mPreviewZoomOnly) {
- extras = DRAW_EXTRAS_SELECTION;
- nativeSetSelectionRegion(mTouchSelection || mExtendSelection);
- nativeSetSelectionPointer(!mTouchSelection, mInvActualScale,
- mSelectX, mSelectY - getTitleHeight(),
- mExtendSelection);
- }
+ extras = DRAW_EXTRAS_FIND;
+ } else if (mSelectingText) {
+ extras = DRAW_EXTRAS_SELECTION;
+ nativeSetSelectionPointer(mDrawSelectionPointer,
+ mZoomManager.getInvScale(),
+ mSelectX, mSelectY - getTitleHeight());
} else if (drawCursorRing) {
extras = DRAW_EXTRAS_CURSOR_RING;
}
- drawExtras(canvas, extras, UIAnimationsRunning);
+ DrawFilter df = null;
+ if (mZoomManager.isZoomAnimating() || UIAnimationsRunning) {
+ df = mZoomFilter;
+ } else if (animateScroll) {
+ df = mScrollFilter;
+ }
+ canvas.setDrawFilter(df);
+ int content = nativeDraw(canvas, color, extras, true);
+ canvas.setDrawFilter(null);
+ if (content != 0) {
+ mWebViewCore.sendMessage(EventHub.SPLIT_PICTURE_SET, content, 0);
+ }
if (extras == DRAW_EXTRAS_CURSOR_RING) {
if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) {
mTouchMode = TOUCH_SHORTPRESS_MODE;
- HitTestResult hitTest = getHitTestResult();
- if (hitTest == null
- || hitTest.mType == HitTestResult.UNKNOWN_TYPE) {
- mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
- }
}
}
if (mFocusSizeChanged) {
@@ -3512,10 +3412,14 @@ public class WebView extends AbsoluteLayout
return mDrawHistory;
}
+ int getHistoryPictureWidth() {
+ return (mHistoryPicture != null) ? mHistoryPicture.getWidth() : 0;
+ }
+
// Should only be called in UI thread
void switchOutDrawHistory() {
if (null == mWebViewCore) return; // CallbackProxy may trigger this
- if (mDrawHistory && mWebViewCore.pictureReady()) {
+ if (mDrawHistory && (getProgress() == 100 || nativeHasContent())) {
mDrawHistory = false;
mHistoryPicture = null;
invalidate();
@@ -3566,7 +3470,9 @@ public class WebView extends AbsoluteLayout
* @param end End of selection.
*/
/* package */ void setSelection(int start, int end) {
- mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end);
+ if (mWebViewCore != null) {
+ mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end);
+ }
}
@Override
@@ -3585,13 +3491,10 @@ public class WebView extends AbsoluteLayout
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
// bring it back to the default scale so that user can enter text
- boolean zoom = mActualScale < mDefaultScale;
+ boolean zoom = mZoomManager.getScale() < mZoomManager.getDefaultScale();
if (zoom) {
- mInZoomOverview = false;
- mZoomCenterX = mLastTouchX;
- mZoomCenterY = mLastTouchY;
- // do not change text wrap scale so that there is no reflow
- setNewZoomScale(mDefaultScale, false, false);
+ mZoomManager.setZoomCenter(mLastTouchX, mLastTouchY);
+ mZoomManager.setZoomScale(mZoomManager.getDefaultScale(), false);
}
if (isTextView) {
rebuildWebTextView();
@@ -3817,17 +3720,19 @@ public class WebView extends AbsoluteLayout
// Bubble up the key event if
// 1. it is a system key; or
// 2. the host application wants to handle it;
+ // 3. the accessibility injector is present and wants to handle it;
if (event.isSystem()
- || mCallbackProxy.uiOverrideKeyEvent(event)) {
+ || mCallbackProxy.uiOverrideKeyEvent(event)
+ || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
|| keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
- if (nativeFocusIsPlugin()) {
+ if (nativePageShouldHandleShiftAndArrows()) {
mShiftIsPressed = true;
- } else if (!nativeCursorWantsKeyEvents() && !mShiftIsPressed) {
- setUpSelectXY();
+ } else if (!nativeCursorWantsKeyEvents() && !mSelectingText) {
+ setUpSelect();
}
}
@@ -3844,11 +3749,11 @@ public class WebView extends AbsoluteLayout
if (keyCode >= KeyEvent.KEYCODE_DPAD_UP
&& keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) {
switchOutDrawHistory();
- if (nativeFocusIsPlugin()) {
- letPluginHandleNavKey(keyCode, event.getEventTime(), true);
+ if (nativePageShouldHandleShiftAndArrows()) {
+ letPageHandleNavKey(keyCode, event.getEventTime(), true);
return true;
}
- if (mShiftIsPressed) {
+ if (mSelectingText) {
int xRate = keyCode == KeyEvent.KEYCODE_DPAD_LEFT
? -1 : keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ? 1 : 0;
int yRate = keyCode == KeyEvent.KEYCODE_DPAD_UP ?
@@ -3868,7 +3773,7 @@ public class WebView extends AbsoluteLayout
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
switchOutDrawHistory();
if (event.getRepeatCount() == 0) {
- if (mShiftIsPressed && !nativeFocusIsPlugin()) {
+ if (mSelectingText) {
return true; // discard press if copy in progress
}
mGotCenterDown = true;
@@ -3886,10 +3791,8 @@ public class WebView extends AbsoluteLayout
if (keyCode != KeyEvent.KEYCODE_SHIFT_LEFT
&& keyCode != KeyEvent.KEYCODE_SHIFT_RIGHT) {
// turn off copy select if a shift-key combo is pressed
- mExtendSelection = mShiftIsPressed = false;
- if (mTouchMode == TOUCH_SELECT_MODE) {
- mTouchMode = TOUCH_INIT_MODE;
- }
+ selectionDone();
+ mShiftIsPressed = false;
}
if (getSettings().getNavDump()) {
@@ -3971,23 +3874,27 @@ public class WebView extends AbsoluteLayout
// Bubble up the key event if
// 1. it is a system key; or
// 2. the host application wants to handle it;
- if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) {
+ // 3. the accessibility injector is present and wants to handle it;
+ if (event.isSystem()
+ || mCallbackProxy.uiOverrideKeyEvent(event)
+ || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) {
return false;
}
if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
|| keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
- if (nativeFocusIsPlugin()) {
+ if (nativePageShouldHandleShiftAndArrows()) {
mShiftIsPressed = false;
- } else if (commitCopy()) {
+ } else if (copySelection()) {
+ selectionDone();
return true;
}
}
if (keyCode >= KeyEvent.KEYCODE_DPAD_UP
&& keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) {
- if (nativeFocusIsPlugin()) {
- letPluginHandleNavKey(keyCode, event.getEventTime(), false);
+ if (nativePageShouldHandleShiftAndArrows()) {
+ letPageHandleNavKey(keyCode, event.getEventTime(), false);
return true;
}
// always handle the navigation keys in the UI thread
@@ -4000,11 +3907,13 @@ public class WebView extends AbsoluteLayout
mPrivateHandler.removeMessages(LONG_PRESS_CENTER);
mGotCenterDown = false;
- if (mShiftIsPressed && !nativeFocusIsPlugin()) {
+ if (mSelectingText) {
if (mExtendSelection) {
- commitCopy();
+ copySelection();
+ selectionDone();
} else {
mExtendSelection = true;
+ nativeSetExtendSelection();
invalidate(); // draw the i-beam instead of the arrow
}
return true; // discard press if copy in progress
@@ -4049,9 +3958,18 @@ public class WebView extends AbsoluteLayout
return false;
}
- private void setUpSelectXY() {
+ /**
+ * @hide pending API council approval.
+ */
+ public void setUpSelect() {
+ if (0 == mNativeClass) return; // client isn't initialized
+ if (inFullScreenMode()) return;
+ if (mSelectingText) return;
mExtendSelection = false;
- mShiftIsPressed = true;
+ mSelectingText = mDrawSelectionPointer = true;
+ // don't let the picture change during text selection
+ WebViewCore.pauseUpdatePicture(mWebViewCore);
+ nativeResetSelection();
if (nativeHasCursorNode()) {
Rect rect = nativeCursorNodeBounds();
mSelectX = contentToViewX(rect.left);
@@ -4071,40 +3989,82 @@ public class WebView extends AbsoluteLayout
* Do not rely on this functionality; it will be deprecated in the future.
*/
public void emulateShiftHeld() {
+ setUpSelect();
+ }
+
+ /**
+ * @hide pending API council approval.
+ */
+ public void selectAll() {
if (0 == mNativeClass) return; // client isn't initialized
- setUpSelectXY();
+ if (inFullScreenMode()) return;
+ if (!mSelectingText) setUpSelect();
+ nativeSelectAll();
+ mDrawSelectionPointer = false;
+ mExtendSelection = true;
+ invalidate();
}
- private boolean commitCopy() {
+ /**
+ * @hide pending API council approval.
+ */
+ public boolean selectDialogIsUp() {
+ return mSelectingText;
+ }
+
+ /**
+ * @hide pending API council approval.
+ */
+ public void notifySelectDialogDismissed() {
+ mSelectingText = false;
+ WebViewCore.resumeUpdatePicture(mWebViewCore);
+ }
+
+ /**
+ * @hide pending API council approval.
+ */
+ public void selectionDone() {
+ if (mSelectingText) {
+ getWebChromeClient().onSelectionDone();
+ invalidate(); // redraw without selection
+ notifySelectDialogDismissed();
+ }
+ }
+
+ /**
+ * @hide pending API council approval.
+ */
+ public boolean copySelection() {
boolean copiedSomething = false;
- if (mExtendSelection) {
- String selection = nativeGetSelection();
- if (selection != "") {
- if (DebugFlags.WEB_VIEW) {
- Log.v(LOGTAG, "commitCopy \"" + selection + "\"");
- }
- Toast.makeText(mContext
- , com.android.internal.R.string.text_copied
- , Toast.LENGTH_SHORT).show();
- copiedSomething = true;
- try {
- IClipboard clip = IClipboard.Stub.asInterface(
- ServiceManager.getService("clipboard"));
- clip.setClipboardText(selection);
- } catch (android.os.RemoteException e) {
- Log.e(LOGTAG, "Clipboard failed", e);
- }
+ String selection = getSelection();
+ if (selection != "") {
+ if (DebugFlags.WEB_VIEW) {
+ Log.v(LOGTAG, "copySelection \"" + selection + "\"");
+ }
+ Toast.makeText(mContext
+ , com.android.internal.R.string.text_copied
+ , Toast.LENGTH_SHORT).show();
+ copiedSomething = true;
+ try {
+ IClipboard clip = IClipboard.Stub.asInterface(
+ ServiceManager.getService("clipboard"));
+ clip.setClipboardText(selection);
+ } catch (android.os.RemoteException e) {
+ Log.e(LOGTAG, "Clipboard failed", e);
}
- mExtendSelection = false;
}
- mShiftIsPressed = false;
invalidate(); // remove selection region and pointer
- if (mTouchMode == TOUCH_SELECT_MODE) {
- mTouchMode = TOUCH_INIT_MODE;
- }
return copiedSomething;
}
+ /**
+ * @hide pending API council approval.
+ */
+ public String getSelection() {
+ if (mNativeClass == 0) return "";
+ return nativeGetSelection();
+ }
+
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -4114,11 +4074,21 @@ public class WebView extends AbsoluteLayout
@Override
protected void onDetachedFromWindow() {
clearTextEntry(false);
- dismissZoomControl();
+ mZoomManager.dismissZoomPicker();
if (hasWindowFocus()) setActive(false);
super.onDetachedFromWindow();
}
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ // The zoomManager may be null if the webview is created from XML that
+ // specifies the view's visibility param as not visible (see http://b/2794841)
+ if (visibility != View.VISIBLE && mZoomManager != null) {
+ mZoomManager.dismissZoomPicker();
+ }
+ }
+
/**
* @deprecated WebView no longer needs to implement
* ViewGroup.OnHierarchyChangeListener. This method does nothing now.
@@ -4163,17 +4133,14 @@ public class WebView extends AbsoluteLayout
// false for the first parameter
}
} else {
- if (mWebViewCore != null && getSettings().getBuiltInZoomControls()
- && (mZoomButtonsController == null ||
- !mZoomButtonsController.isVisible())) {
+ if (!mZoomManager.isZoomPickerVisible()) {
/*
- * The zoom controls come in their own window, so our window
- * loses focus. Our policy is to not draw the cursor ring if
- * our window is not focused, but this is an exception since
+ * The external zoom controls come in their own window, so our
+ * window loses focus. Our policy is to not draw the cursor ring
+ * if our window is not focused, but this is an exception since
* the user can still navigate the web page with the zoom
* controls showing.
*/
- // If our window has lost focus, stop drawing the cursor ring
mDrawCursorRing = false;
}
mGotKeyDown = false;
@@ -4262,81 +4229,24 @@ public class WebView extends AbsoluteLayout
// system won't call onSizeChanged if the dimension is not changed.
// In this case, we need to call sendViewSizeZoom() explicitly to
// notify the WebKit about the new dimensions.
- sendViewSizeZoom();
+ sendViewSizeZoom(false);
}
return changed;
}
- private static class PostScale implements Runnable {
- final WebView mWebView;
- final boolean mUpdateTextWrap;
-
- public PostScale(WebView webView, boolean updateTextWrap) {
- mWebView = webView;
- mUpdateTextWrap = updateTextWrap;
- }
-
- public void run() {
- if (mWebView.mWebViewCore != null) {
- // we always force, in case our height changed, in which case we
- // still want to send the notification over to webkit.
- mWebView.setNewZoomScale(mWebView.mActualScale,
- mUpdateTextWrap, true);
- // update the zoom buttons as the scale can be changed
- if (mWebView.getSettings().getBuiltInZoomControls()) {
- mWebView.updateZoomButtonsEnabled();
- }
- }
- }
- }
-
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
- // Center zooming to the center of the screen.
- if (mZoomScale == 0) { // unless we're already zooming
- // To anchor at top left corner.
- mZoomCenterX = 0;
- mZoomCenterY = getVisibleTitleHeight();
- mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX);
- mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY);
- }
// adjust the max viewport width depending on the view dimensions. This
// is to ensure the scaling is not going insane. So do not shrink it if
// the view size is temporarily smaller, e.g. when soft keyboard is up.
- int newMaxViewportWidth = (int) (Math.max(w, h) / DEFAULT_MIN_ZOOM_SCALE);
+ int newMaxViewportWidth = (int) (Math.max(w, h) / mZoomManager.getDefaultMinZoomScale());
if (newMaxViewportWidth > sMaxViewportWidth) {
sMaxViewportWidth = newMaxViewportWidth;
}
- // update mMinZoomScale if the minimum zoom scale is not fixed
- if (!mMinZoomScaleFixed) {
- // when change from narrow screen to wide screen, the new viewWidth
- // can be wider than the old content width. We limit the minimum
- // scale to 1.0f. The proper minimum scale will be calculated when
- // the new picture shows up.
- mMinZoomScale = Math.min(1.0f, (float) getViewWidth()
- / (mDrawHistory ? mHistoryPicture.getWidth()
- : mZoomOverviewWidth));
- if (mInitialScaleInPercent > 0) {
- // limit the minZoomScale to the initialScale if it is set
- float initialScale = mInitialScaleInPercent / 100.0f;
- if (mMinZoomScale > initialScale) {
- mMinZoomScale = initialScale;
- }
- }
- }
-
- dismissZoomControl();
-
- // onSizeChanged() is called during WebView layout. And any
- // requestLayout() is blocked during layout. As setNewZoomScale() will
- // call its child View to reposition itself through ViewManager's
- // scaleAll(), we need to post a Runnable to ensure requestLayout().
- // <b/>
- // only update the text wrap scale if width changed.
- post(new PostScale(this, w != ow));
+ mZoomManager.onSizeChanged(w, h, ow, oh);
}
@Override
@@ -4347,7 +4257,7 @@ public class WebView extends AbsoluteLayout
// as getVisibleTitleHeight.
int titleHeight = getTitleHeight();
if (Math.max(titleHeight - t, 0) != Math.max(titleHeight - oldt, 0)) {
- sendViewSizeZoom();
+ sendViewSizeZoom(false);
}
}
@@ -4355,9 +4265,11 @@ public class WebView extends AbsoluteLayout
public boolean dispatchKeyEvent(KeyEvent event) {
boolean dispatch = true;
- // Textfields and plugins need to receive the shift up key even if
- // another key was released while the shift key was held down.
- if (!inEditingMode() && (mNativeClass == 0 || !nativeFocusIsPlugin())) {
+ // Textfields, plugins, and contentEditable nodes need to receive the
+ // shift up key even if another key was released while the shift key
+ // was held down.
+ if (!inEditingMode() && (mNativeClass == 0
+ || !nativePageShouldHandleShiftAndArrows())) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
mGotKeyDown = true;
} else {
@@ -4591,81 +4503,6 @@ public class WebView extends AbsoluteLayout
private DragTracker mDragTracker;
private DragTrackerHandler mDragTrackerHandler;
- private class ScaleDetectorListener implements
- ScaleGestureDetector.OnScaleGestureListener {
-
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- // cancel the single touch handling
- cancelTouch();
- dismissZoomControl();
- // reset the zoom overview mode so that the page won't auto grow
- mInZoomOverview = false;
- // If it is in password mode, turn it off so it does not draw
- // misplaced.
- if (inEditingMode() && nativeFocusCandidateIsPassword()) {
- mWebTextView.setInPassword(false);
- }
-
- mViewManager.startZoom();
-
- return true;
- }
-
- public void onScaleEnd(ScaleGestureDetector detector) {
- if (mPreviewZoomOnly) {
- mPreviewZoomOnly = false;
- mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX);
- mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY);
- // don't reflow when zoom in; when zoom out, do reflow if the
- // new scale is almost minimum scale;
- boolean reflowNow = (mActualScale - mMinZoomScale
- <= MINIMUM_SCALE_INCREMENT)
- || ((mActualScale <= 0.8 * mTextWrapScale));
- // force zoom after mPreviewZoomOnly is set to false so that the
- // new view size will be passed to the WebKit
- setNewZoomScale(mActualScale, reflowNow, true);
- // call invalidate() to draw without zoom filter
- invalidate();
- }
- // adjust the edit text view if needed
- if (inEditingMode() && didUpdateTextViewBounds(false)
- && nativeFocusCandidateIsPassword()) {
- // If it is a password field, start drawing the
- // WebTextView once again.
- mWebTextView.setInPassword(true);
- }
- // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as it
- // may trigger the unwanted click, can't use TOUCH_DRAG_MODE as it
- // may trigger the unwanted fling.
- mTouchMode = TOUCH_PINCH_DRAG;
- mConfirmMove = true;
- startTouch(detector.getFocusX(), detector.getFocusY(),
- mLastTouchTime);
-
- mViewManager.endZoom();
- }
-
- public boolean onScale(ScaleGestureDetector detector) {
- float scale = (float) (Math.round(detector.getScaleFactor()
- * mActualScale * 100) / 100.0);
- if (Math.abs(scale - mActualScale) >= MINIMUM_SCALE_INCREMENT) {
- mPreviewZoomOnly = true;
- // limit the scale change per step
- if (scale > mActualScale) {
- scale = Math.min(scale, mActualScale * 1.25f);
- } else {
- scale = Math.max(scale, mActualScale * 0.8f);
- }
- mZoomCenterX = detector.getFocusX();
- mZoomCenterY = detector.getFocusY();
- setNewZoomScale(scale, false, false);
- invalidate();
- return true;
- }
- return false;
- }
- }
-
private boolean hitFocusedPlugin(int contentX, int contentY) {
if (DebugFlags.WEB_VIEW) {
Log.v(LOGTAG, "nativeFocusIsPlugin()=" + nativeFocusIsPlugin());
@@ -4679,7 +4516,7 @@ public class WebView extends AbsoluteLayout
private boolean shouldForwardTouchEvent() {
return mFullScreenHolder != null || (mForwardTouchEvents
- && mTouchMode != TOUCH_SELECT_MODE
+ && !mSelectingText
&& mPreventDefault != PREVENT_DEFAULT_IGNORE);
}
@@ -4687,6 +4524,22 @@ public class WebView extends AbsoluteLayout
return mFullScreenHolder != null;
}
+ void onPinchToZoomAnimationStart() {
+ // cancel the single touch handling
+ cancelTouch();
+ onZoomAnimationStart();
+ }
+
+ void onPinchToZoomAnimationEnd(ScaleGestureDetector detector) {
+ onZoomAnimationEnd();
+ // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as
+ // it may trigger the unwanted click, can't use TOUCH_DRAG_MODE
+ // as it may trigger the unwanted fling.
+ mTouchMode = TOUCH_PINCH_DRAG;
+ mConfirmMove = true;
+ startTouch(detector.getFocusX(), detector.getFocusY(), mLastTouchTime);
+ }
+
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mNativeClass == 0 || !isClickable() || !isLongClickable()) {
@@ -4704,32 +4557,36 @@ public class WebView extends AbsoluteLayout
// FIXME: we may consider to give WebKit an option to handle multi-touch
// events later.
- if (mSupportMultiTouch && ev.getPointerCount() > 1) {
- if (mMinZoomScale < mMaxZoomScale) {
- mScaleDetector.onTouchEvent(ev);
- if (mScaleDetector.isInProgress()) {
- mLastTouchTime = eventTime;
+ if (mZoomManager.supportsMultiTouchZoom() && ev.getPointerCount() > 1) {
+
+ // if the page disallows zoom, then skip multi-pointer action
+ if (mZoomManager.isZoomScaleFixed()) {
+ return true;
+ }
+
+ ScaleGestureDetector detector = mZoomManager.getMultiTouchGestureDetector();
+ detector.onTouchEvent(ev);
+
+ if (detector.isInProgress()) {
+ mLastTouchTime = eventTime;
+ return true;
+ }
+
+ x = detector.getFocusX();
+ y = detector.getFocusY();
+ action = ev.getAction() & MotionEvent.ACTION_MASK;
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ cancelTouch();
+ action = MotionEvent.ACTION_DOWN;
+ } else if (action == MotionEvent.ACTION_POINTER_UP) {
+ // set mLastTouchX/Y to the remaining point
+ mLastTouchX = x;
+ mLastTouchY = y;
+ } else if (action == MotionEvent.ACTION_MOVE) {
+ // negative x or y indicate it is on the edge, skip it.
+ if (x < 0 || y < 0) {
return true;
}
- x = mScaleDetector.getFocusX();
- y = mScaleDetector.getFocusY();
- action = ev.getAction() & MotionEvent.ACTION_MASK;
- if (action == MotionEvent.ACTION_POINTER_DOWN) {
- cancelTouch();
- action = MotionEvent.ACTION_DOWN;
- } else if (action == MotionEvent.ACTION_POINTER_UP) {
- // set mLastTouchX/Y to the remaining point
- mLastTouchX = x;
- mLastTouchY = y;
- } else if (action == MotionEvent.ACTION_MOVE) {
- // negative x or y indicate it is on the edge, skip it.
- if (x < 0 || y < 0) {
- return true;
- }
- }
- } else {
- // if the page disallow zoom, skip multi-pointer action
- return true;
}
} else {
action = ev.getAction();
@@ -4767,18 +4624,11 @@ public class WebView extends AbsoluteLayout
mTouchMode = TOUCH_DRAG_START_MODE;
mConfirmMove = true;
mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY);
- } else if (!inFullScreenMode() && mShiftIsPressed) {
- mSelectX = mScrollX + (int) x;
- mSelectY = mScrollY + (int) y;
- mTouchMode = TOUCH_SELECT_MODE;
- if (DebugFlags.WEB_VIEW) {
- Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY);
- }
- nativeMoveSelection(contentX, contentY, false);
- mTouchSelection = mExtendSelection = true;
- invalidate(); // draw the i-beam instead of the arrow
} else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) {
mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP);
+ if (getSettings().supportTouchOnly()) {
+ removeTouchHighlight(true);
+ }
if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) {
mTouchMode = TOUCH_DOUBLE_TAP_MODE;
} else {
@@ -4790,17 +4640,45 @@ public class WebView extends AbsoluteLayout
contentX, contentY) : false;
}
} else { // the normal case
- mPreviewZoomOnly = false;
mTouchMode = TOUCH_INIT_MODE;
mDeferTouchProcess = (!inFullScreenMode()
&& mForwardTouchEvents) ? hitFocusedPlugin(
contentX, contentY) : false;
mWebViewCore.sendMessage(
EventHub.UPDATE_FRAME_CACHE_IF_LOADING);
+ if (getSettings().supportTouchOnly()) {
+ TouchHighlightData data = new TouchHighlightData();
+ data.mX = contentX;
+ data.mY = contentY;
+ data.mSlop = viewToContentDimension(mNavSlop);
+ mWebViewCore.sendMessageDelayed(
+ EventHub.GET_TOUCH_HIGHLIGHT_RECTS, data,
+ ViewConfiguration.getTapTimeout());
+ if (DEBUG_TOUCH_HIGHLIGHT) {
+ if (getSettings().getNavDump()) {
+ mTouchHighlightX = (int) x + mScrollX;
+ mTouchHighlightY = (int) y + mScrollY;
+ mPrivateHandler.postDelayed(new Runnable() {
+ public void run() {
+ mTouchHighlightX = mTouchHighlightY = 0;
+ invalidate();
+ }
+ }, TOUCH_HIGHLIGHT_ELAPSE_TIME);
+ }
+ }
+ }
if (mLogEvent && eventTime - mLastTouchUpTime < 1000) {
EventLog.writeEvent(EventLogTags.BROWSER_DOUBLE_TAP_DURATION,
(eventTime - mLastTouchUpTime), eventTime);
}
+ if (mSelectingText) {
+ mDrawSelectionPointer = false;
+ mSelectionStarted = nativeStartSelection(contentX, contentY);
+ if (DebugFlags.WEB_VIEW) {
+ Log.v(LOGTAG, "select=" + contentX + "," + contentY);
+ }
+ invalidate();
+ }
}
// Trigger the link
if (mTouchMode == TOUCH_INIT_MODE
@@ -4824,17 +4702,15 @@ public class WebView extends AbsoluteLayout
ted.mY = contentY;
ted.mMetaState = ev.getMetaState();
ted.mReprocess = mDeferTouchProcess;
+ mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
if (mDeferTouchProcess) {
// still needs to set them for compute deltaX/Y
mLastTouchX = x;
mLastTouchY = y;
- ted.mViewX = x;
- ted.mViewY = y;
- mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
break;
}
- mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
if (!inFullScreenMode()) {
+ mPrivateHandler.removeMessages(PREVENT_DEFAULT_TIMEOUT);
mPrivateHandler.sendMessageDelayed(mPrivateHandler
.obtainMessage(PREVENT_DEFAULT_TIMEOUT,
action, 0), TAP_TIMEOUT);
@@ -4855,24 +4731,24 @@ public class WebView extends AbsoluteLayout
if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) {
mTouchMode = TOUCH_INIT_MODE;
}
+ if (getSettings().supportTouchOnly()) {
+ removeTouchHighlight(true);
+ }
}
// pass the touch events from UI thread to WebCore thread
if (shouldForwardTouchEvent() && mConfirmMove && (firstMove
|| eventTime - mLastSentTouchTime > mCurrentTouchInterval)) {
- mLastSentTouchTime = eventTime;
TouchEventData ted = new TouchEventData();
ted.mAction = action;
ted.mX = contentX;
ted.mY = contentY;
ted.mMetaState = ev.getMetaState();
ted.mReprocess = mDeferTouchProcess;
+ mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
+ mLastSentTouchTime = eventTime;
if (mDeferTouchProcess) {
- ted.mViewX = x;
- ted.mViewY = y;
- mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
break;
}
- mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
if (firstMove && !inFullScreenMode()) {
mPrivateHandler.sendMessageDelayed(mPrivateHandler
.obtainMessage(PREVENT_DEFAULT_TIMEOUT,
@@ -4892,20 +4768,20 @@ public class WebView extends AbsoluteLayout
+ " mTouchMode = " + mTouchMode);
}
mVelocityTracker.addMovement(ev);
- if (mTouchMode != TOUCH_DRAG_MODE) {
- if (mTouchMode == TOUCH_SELECT_MODE) {
- mSelectX = mScrollX + (int) x;
- mSelectY = mScrollY + (int) y;
- if (DebugFlags.WEB_VIEW) {
- Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY);
- }
- nativeMoveSelection(contentX, contentY, true);
- invalidate();
- break;
+ if (mSelectingText && mSelectionStarted) {
+ if (DebugFlags.WEB_VIEW) {
+ Log.v(LOGTAG, "extend=" + contentX + "," + contentY);
}
+ nativeExtendSelection(contentX, contentY);
+ invalidate();
+ break;
+ }
+ if (mTouchMode != TOUCH_DRAG_MODE) {
+
if (!mConfirmMove) {
break;
}
+
if (mPreventDefault == PREVENT_DEFAULT_MAYBE_YES
|| mPreventDefault == PREVENT_DEFAULT_NO_FROM_TOUCH_DOWN) {
// track mLastTouchTime as we may need to do fling at
@@ -5033,6 +4909,7 @@ public class WebView extends AbsoluteLayout
break;
}
case MotionEvent.ACTION_UP: {
+ if (!isFocused()) requestFocus();
// pass the touch events from UI thread to WebCore thread
if (shouldForwardTouchEvent()) {
TouchEventData ted = new TouchEventData();
@@ -5041,10 +4918,6 @@ public class WebView extends AbsoluteLayout
ted.mY = contentY;
ted.mMetaState = ev.getMetaState();
ted.mReprocess = mDeferTouchProcess;
- if (mDeferTouchProcess) {
- ted.mViewX = x;
- ted.mViewY = y;
- }
mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
}
mLastTouchUpTime = eventTime;
@@ -5059,20 +4932,12 @@ public class WebView extends AbsoluteLayout
ted.mY = contentY;
ted.mMetaState = ev.getMetaState();
ted.mReprocess = mDeferTouchProcess;
- if (mDeferTouchProcess) {
- ted.mViewX = x;
- ted.mViewY = y;
- }
mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
} else if (mPreventDefault != PREVENT_DEFAULT_YES){
- doDoubleTap();
+ mZoomManager.handleDoubleTap(mLastTouchX, mLastTouchY);
mTouchMode = TOUCH_DONE_MODE;
}
break;
- case TOUCH_SELECT_MODE:
- commitCopy();
- mTouchSelection = false;
- break;
case TOUCH_INIT_MODE: // tap
case TOUCH_SHORTPRESS_START_MODE:
case TOUCH_SHORTPRESS_MODE:
@@ -5084,9 +4949,17 @@ public class WebView extends AbsoluteLayout
if (mPreventDefault != PREVENT_DEFAULT_YES
&& (computeMaxScrollX() > 0
|| computeMaxScrollY() > 0)) {
- // UI takes control back, cancel WebCore touch
- cancelWebCoreTouchEvent(contentX, contentY,
- true);
+ // If the user has performed a very quick touch
+ // sequence it is possible that we may get here
+ // before WebCore has had a chance to process the events.
+ // In this case, any call to preventDefault in the
+ // JS touch handler will not have been executed yet.
+ // Hence we will see both the UI (now) and WebCore
+ // (when context switches) handling the event,
+ // regardless of whether the web developer actually
+ // doeses preventDefault in their touch handler. This
+ // is the nature of our asynchronous touch model.
+
// we will not rewrite drag code here, but we
// will try fling if it applies.
WebViewCore.reducePriority();
@@ -5103,7 +4976,17 @@ public class WebView extends AbsoluteLayout
break;
}
} else {
- if (mTouchMode == TOUCH_INIT_MODE) {
+ if (mSelectingText) {
+ // tapping on selection or controls does nothing
+ if (!nativeHitSelection(contentX, contentY)) {
+ selectionDone();
+ }
+ break;
+ }
+ // only trigger double tap if the WebView is
+ // scalable
+ if (mTouchMode == TOUCH_INIT_MODE
+ && (canZoomIn() || canZoomOut())) {
mPrivateHandler.sendEmptyMessageDelayed(
RELEASE_SINGLE_TAP, ViewConfiguration
.getDoubleTapTimeout());
@@ -5194,21 +5077,10 @@ public class WebView extends AbsoluteLayout
if (!mDragFromTextInput) {
nativeHideCursor();
}
- WebSettings settings = getSettings();
- if (settings.supportZoom()
- && settings.getBuiltInZoomControls()
- && !getZoomButtonsController().isVisible()
- && mMinZoomScale < mMaxZoomScale
- && (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF
- || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF)) {
- mZoomButtonsController.setVisible(true);
- int count = settings.getDoubleTapToastCount();
- if (mInZoomOverview && count > 0) {
- settings.setDoubleTapToastCount(--count);
- Toast.makeText(mContext,
- com.android.internal.R.string.double_tap_toast,
- Toast.LENGTH_LONG).show();
- }
+
+ if (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF
+ || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF) {
+ mZoomManager.invokeZoomPicker();
}
}
@@ -5216,18 +5088,7 @@ public class WebView extends AbsoluteLayout
if ((deltaX | deltaY) != 0) {
scrollBy(deltaX, deltaY);
}
- if (!getSettings().getBuiltInZoomControls()) {
- boolean showPlusMinus = mMinZoomScale < mMaxZoomScale;
- if (mZoomControls != null && showPlusMinus) {
- if (mZoomControls.getVisibility() == View.VISIBLE) {
- mPrivateHandler.removeCallbacks(mZoomControlRunnable);
- } else {
- mZoomControls.show(showPlusMinus, false);
- }
- mPrivateHandler.postDelayed(mZoomControlRunnable,
- ZOOM_CONTROLS_TIMEOUT);
- }
- }
+ mZoomManager.keepZoomPickerVisible();
}
private void stopTouch() {
@@ -5262,6 +5123,9 @@ public class WebView extends AbsoluteLayout
mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS);
mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS);
+ if (getSettings().supportTouchOnly()) {
+ removeTouchHighlight(true);
+ }
mHeldMotionless = MOTIONLESS_TRUE;
mTouchMode = TOUCH_DONE_MODE;
nativeHideCursor();
@@ -5273,8 +5137,10 @@ public class WebView extends AbsoluteLayout
private float mTrackballRemainsY = 0.0f;
private int mTrackballXMove = 0;
private int mTrackballYMove = 0;
+ private boolean mSelectingText = false;
+ private boolean mSelectionStarted = false;
private boolean mExtendSelection = false;
- private boolean mTouchSelection = false;
+ private boolean mDrawSelectionPointer = false;
private static final int TRACKBALL_KEY_TIMEOUT = 1000;
private static final int TRACKBALL_TIMEOUT = 200;
private static final int TRACKBALL_WAIT = 100;
@@ -5313,10 +5179,8 @@ public class WebView extends AbsoluteLayout
if (ev.getY() < 0) pageUp(true);
return true;
}
- boolean shiftPressed = mShiftIsPressed && (mNativeClass == 0
- || !nativeFocusIsPlugin());
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- if (shiftPressed) {
+ if (mSelectingText) {
return true; // discard press if copy in progress
}
mTrackballDown = true;
@@ -5341,11 +5205,13 @@ public class WebView extends AbsoluteLayout
mPrivateHandler.removeMessages(LONG_PRESS_CENTER);
mTrackballDown = false;
mTrackballUpTime = time;
- if (shiftPressed) {
+ if (mSelectingText) {
if (mExtendSelection) {
- commitCopy();
+ copySelection();
+ selectionDone();
} else {
mExtendSelection = true;
+ nativeSetExtendSelection();
invalidate(); // draw the i-beam instead of the arrow
}
return true; // discard press if copy in progress
@@ -5412,8 +5278,7 @@ public class WebView extends AbsoluteLayout
+ " yRate=" + yRate
);
}
- nativeMoveSelection(viewToContentX(mSelectX),
- viewToContentY(mSelectY), mExtendSelection);
+ nativeMoveSelection(viewToContentX(mSelectX), viewToContentY(mSelectY));
int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET
: mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET
: 0;
@@ -5479,7 +5344,16 @@ public class WebView extends AbsoluteLayout
float yRate = mTrackballRemainsY * 1000 / elapsed;
int viewWidth = getViewWidth();
int viewHeight = getViewHeight();
- if (mShiftIsPressed && (mNativeClass == 0 || !nativeFocusIsPlugin())) {
+ if (mSelectingText) {
+ if (!mDrawSelectionPointer) {
+ // The last selection was made by touch, disabling drawing the
+ // selection pointer. Allow the trackball to adjust the
+ // position of the touch control.
+ mSelectX = contentToViewX(nativeSelectionX());
+ mSelectY = contentToViewY(nativeSelectionY());
+ mDrawSelectionPointer = mExtendSelection = true;
+ nativeSetExtendSelection();
+ }
moveSelection(scaleTrackballX(xRate, viewWidth),
scaleTrackballY(yRate, viewHeight));
mTrackballRemainsX = mTrackballRemainsY = 0;
@@ -5517,11 +5391,11 @@ public class WebView extends AbsoluteLayout
+ " mTrackballRemainsX=" + mTrackballRemainsX
+ " mTrackballRemainsY=" + mTrackballRemainsY);
}
- if (mNativeClass != 0 && nativeFocusIsPlugin()) {
+ if (mNativeClass != 0 && nativePageShouldHandleShiftAndArrows()) {
for (int i = 0; i < count; i++) {
- letPluginHandleNavKey(selectKeyCode, time, true);
+ letPageHandleNavKey(selectKeyCode, time, true);
}
- letPluginHandleNavKey(selectKeyCode, time, false);
+ letPageHandleNavKey(selectKeyCode, time, false);
} else if (navHandledKey(selectKeyCode, count, false, time)) {
playSoundEffect(keyCodeToSoundsEffect(selectKeyCode));
}
@@ -5560,6 +5434,19 @@ public class WebView extends AbsoluteLayout
- getViewHeightWithTitle(), 0);
}
+ boolean updateScrollCoordinates(int x, int y) {
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ mScrollX = x;
+ mScrollY = y;
+ if (oldX != mScrollX || oldY != mScrollY) {
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
public void flingScroll(int vx, int vy) {
mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0,
computeMaxScrollY());
@@ -5595,13 +5482,16 @@ public class WebView extends AbsoluteLayout
return;
}
float currentVelocity = mScroller.getCurrVelocity();
- if (mLastVelocity > 0 && currentVelocity > 0) {
+ float velocity = (float) Math.hypot(vx, vy);
+ if (mLastVelocity > 0 && currentVelocity > 0 && velocity
+ > mLastVelocity * MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION) {
float deltaR = (float) (Math.abs(Math.atan2(mLastVelY, mLastVelX)
- Math.atan2(vy, vx)));
final float circle = (float) (Math.PI) * 2.0f;
if (deltaR > circle * 0.9f || deltaR < circle * 0.1f) {
vx += currentVelocity * mLastVelX / mLastVelocity;
vy += currentVelocity * mLastVelY / mLastVelocity;
+ velocity = (float) Math.hypot(vx, vy);
if (DebugFlags.WEB_VIEW) {
Log.v(LOGTAG, "doFling vx= " + vx + " vy=" + vy);
}
@@ -5617,45 +5507,15 @@ public class WebView extends AbsoluteLayout
}
mLastVelX = vx;
mLastVelY = vy;
- mLastVelocity = (float) Math.hypot(vx, vy);
+ mLastVelocity = velocity;
mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY);
- // TODO: duration is calculated based on velocity, if the range is
- // small, the animation will stop before duration is up. We may
- // want to calculate how long the animation is going to run to precisely
- // resume the webcore update.
final int time = mScroller.getDuration();
mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_PRIORITY, time);
awakenScrollBars(time);
invalidate();
}
- private boolean zoomWithPreview(float scale, boolean updateTextWrapScale) {
- float oldScale = mActualScale;
- mInitialScrollX = mScrollX;
- mInitialScrollY = mScrollY;
-
- // snap to DEFAULT_SCALE if it is close
- if (Math.abs(scale - mDefaultScale) < MINIMUM_SCALE_INCREMENT) {
- scale = mDefaultScale;
- }
-
- setNewZoomScale(scale, updateTextWrapScale, false);
-
- if (oldScale != mActualScale) {
- // use mZoomPickerScale to see zoom preview first
- mZoomStart = SystemClock.uptimeMillis();
- mInvInitialZoomScale = 1.0f / oldScale;
- mInvFinalZoomScale = 1.0f / mActualScale;
- mZoomScale = mActualScale;
- WebViewCore.pauseUpdatePicture(mWebViewCore);
- invalidate();
- return true;
- } else {
- return false;
- }
- }
-
/**
* Returns a view containing zoom controls i.e. +/- buttons. The caller is
* in charge of installing this view to the view hierarchy. This view will
@@ -5675,81 +5535,29 @@ public class WebView extends AbsoluteLayout
Log.w(LOGTAG, "This WebView doesn't support zoom.");
return null;
}
- if (mZoomControls == null) {
- mZoomControls = createZoomControls();
+ return mZoomManager.getExternalZoomPicker();
+ }
- /*
- * need to be set to VISIBLE first so that getMeasuredHeight() in
- * {@link #onSizeChanged()} can return the measured value for proper
- * layout.
- */
- mZoomControls.setVisibility(View.VISIBLE);
- mZoomControlRunnable = new Runnable() {
- public void run() {
+ void dismissZoomControl() {
+ mZoomManager.dismissZoomPicker();
+ }
- /* Don't dismiss the controls if the user has
- * focus on them. Wait and check again later.
- */
- if (!mZoomControls.hasFocus()) {
- mZoomControls.hide();
- } else {
- mPrivateHandler.removeCallbacks(mZoomControlRunnable);
- mPrivateHandler.postDelayed(mZoomControlRunnable,
- ZOOM_CONTROLS_TIMEOUT);
- }
- }
- };
- }
- return mZoomControls;
+ float getDefaultZoomScale() {
+ return mZoomManager.getDefaultScale();
}
- private ExtendedZoomControls createZoomControls() {
- ExtendedZoomControls zoomControls = new ExtendedZoomControls(mContext
- , null);
- zoomControls.setOnZoomInClickListener(new OnClickListener() {
- public void onClick(View v) {
- // reset time out
- mPrivateHandler.removeCallbacks(mZoomControlRunnable);
- mPrivateHandler.postDelayed(mZoomControlRunnable,
- ZOOM_CONTROLS_TIMEOUT);
- zoomIn();
- }
- });
- zoomControls.setOnZoomOutClickListener(new OnClickListener() {
- public void onClick(View v) {
- // reset time out
- mPrivateHandler.removeCallbacks(mZoomControlRunnable);
- mPrivateHandler.postDelayed(mZoomControlRunnable,
- ZOOM_CONTROLS_TIMEOUT);
- zoomOut();
- }
- });
- return zoomControls;
+ /**
+ * @return TRUE if the WebView can be zoomed in.
+ */
+ public boolean canZoomIn() {
+ return mZoomManager.canZoomIn();
}
/**
- * Gets the {@link ZoomButtonsController} which can be used to add
- * additional buttons to the zoom controls window.
- *
- * @return The instance of {@link ZoomButtonsController} used by this class,
- * or null if it is unavailable.
- * @hide
+ * @return TRUE if the WebView can be zoomed out.
*/
- public ZoomButtonsController getZoomButtonsController() {
- if (mZoomButtonsController == null) {
- mZoomButtonsController = new ZoomButtonsController(this);
- mZoomButtonsController.setOnZoomListener(mZoomListener);
- // ZoomButtonsController positions the buttons at the bottom, but in
- // the middle. Change their layout parameters so they appear on the
- // right.
- View controls = mZoomButtonsController.getZoomControls();
- ViewGroup.LayoutParams params = controls.getLayoutParams();
- if (params instanceof FrameLayout.LayoutParams) {
- FrameLayout.LayoutParams frameParams = (FrameLayout.LayoutParams) params;
- frameParams.gravity = Gravity.RIGHT;
- }
- }
- return mZoomButtonsController;
+ public boolean canZoomOut() {
+ return mZoomManager.canZoomOut();
}
/**
@@ -5757,15 +5565,7 @@ public class WebView extends AbsoluteLayout
* @return TRUE if zoom in succeeds. FALSE if no zoom changes.
*/
public boolean zoomIn() {
- // TODO: alternatively we can disallow this during draw history mode
- switchOutDrawHistory();
- mInZoomOverview = false;
- // Center zooming to the center of the screen.
- mZoomCenterX = getViewWidth() * .5f;
- mZoomCenterY = getViewHeight() * .5f;
- mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX);
- mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY);
- return zoomWithPreview(mActualScale * 1.25f, true);
+ return mZoomManager.zoomIn();
}
/**
@@ -5773,14 +5573,7 @@ public class WebView extends AbsoluteLayout
* @return TRUE if zoom out succeeds. FALSE if no zoom changes.
*/
public boolean zoomOut() {
- // TODO: alternatively we can disallow this during draw history mode
- switchOutDrawHistory();
- // Center zooming to the center of the screen.
- mZoomCenterX = getViewWidth() * .5f;
- mZoomCenterY = getViewHeight() * .5f;
- mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX);
- mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY);
- return zoomWithPreview(mActualScale * 0.8f, true);
+ return mZoomManager.zoomOut();
}
private void updateSelection() {
@@ -5881,7 +5674,14 @@ public class WebView extends AbsoluteLayout
// mLastTouchX and mLastTouchY are the point in the current viewport
int contentX = viewToContentX((int) mLastTouchX + mScrollX);
int contentY = viewToContentY((int) mLastTouchY + mScrollY);
- if (nativePointInNavCache(contentX, contentY, mNavSlop)) {
+ if (getSettings().supportTouchOnly()) {
+ removeTouchHighlight(false);
+ WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData();
+ // use "0" as generation id to inform WebKit to use the same x/y as
+ // it used when processing GET_TOUCH_HIGHLIGHT_RECTS
+ touchUpData.mMoveGeneration = 0;
+ mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData);
+ } else if (nativePointInNavCache(contentX, contentY, mNavSlop)) {
WebViewCore.MotionUpData motionUpData = new WebViewCore
.MotionUpData();
motionUpData.mFrame = nativeCacheHitFramePointer();
@@ -5909,27 +5709,16 @@ public class WebView extends AbsoluteLayout
* Return true if the view (Plugin) is fully visible and maximized inside
* the WebView.
*/
- private boolean isPluginFitOnScreen(ViewManager.ChildView view) {
- int viewWidth = getViewWidth();
- int viewHeight = getViewHeightWithTitle();
- float scale = Math.min((float) viewWidth / view.width,
- (float) viewHeight / view.height);
- if (scale < mMinZoomScale) {
- scale = mMinZoomScale;
- } else if (scale > mMaxZoomScale) {
- scale = mMaxZoomScale;
- }
- if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_INCREMENT) {
- if (contentToViewX(view.x) >= mScrollX
- && contentToViewX(view.x + view.width) <= mScrollX
- + viewWidth
- && contentToViewY(view.y) >= mScrollY
- && contentToViewY(view.y + view.height) <= mScrollY
- + viewHeight) {
- return true;
- }
- }
- return false;
+ boolean isPluginFitOnScreen(ViewManager.ChildView view) {
+ final int viewWidth = getViewWidth();
+ final int viewHeight = getViewHeightWithTitle();
+ float scale = Math.min((float) viewWidth / view.width, (float) viewHeight / view.height);
+ scale = mZoomManager.computeScaleWithLimits(scale);
+ return !mZoomManager.willScaleTriggerZoom(scale)
+ && contentToViewX(view.x) >= mScrollX
+ && contentToViewX(view.x + view.width) <= mScrollX + viewWidth
+ && contentToViewY(view.y) >= mScrollY
+ && contentToViewY(view.y + view.height) <= mScrollY + viewHeight;
}
/*
@@ -5938,22 +5727,19 @@ public class WebView extends AbsoluteLayout
* animated scroll to center it. If the zoom needs to be changed, find the
* zoom center and do a smooth zoom transition.
*/
- private void centerFitRect(int docX, int docY, int docWidth, int docHeight) {
+ void centerFitRect(int docX, int docY, int docWidth, int docHeight) {
int viewWidth = getViewWidth();
int viewHeight = getViewHeightWithTitle();
float scale = Math.min((float) viewWidth / docWidth, (float) viewHeight
/ docHeight);
- if (scale < mMinZoomScale) {
- scale = mMinZoomScale;
- } else if (scale > mMaxZoomScale) {
- scale = mMaxZoomScale;
- }
- if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_INCREMENT) {
+ scale = mZoomManager.computeScaleWithLimits(scale);
+ if (!mZoomManager.willScaleTriggerZoom(scale)) {
pinScrollTo(contentToViewX(docX + docWidth / 2) - viewWidth / 2,
contentToViewY(docY + docHeight / 2) - viewHeight / 2,
true, 0);
} else {
- float oldScreenX = docX * mActualScale - mScrollX;
+ float actualScale = mZoomManager.getScale();
+ float oldScreenX = docX * actualScale - mScrollX;
float rectViewX = docX * scale;
float rectViewWidth = docWidth * scale;
float newMaxWidth = mContentWidth * scale;
@@ -5964,9 +5750,9 @@ public class WebView extends AbsoluteLayout
} else if (newScreenX > (newMaxWidth - rectViewX - rectViewWidth)) {
newScreenX = viewWidth - (newMaxWidth - rectViewX);
}
- mZoomCenterX = (oldScreenX * scale - newScreenX * mActualScale)
- / (scale - mActualScale);
- float oldScreenY = docY * mActualScale + getTitleHeight()
+ float zoomCenterX = (oldScreenX * scale - newScreenX * actualScale)
+ / (scale - actualScale);
+ float oldScreenY = docY * actualScale + getTitleHeight()
- mScrollY;
float rectViewY = docY * scale + getTitleHeight();
float rectViewHeight = docHeight * scale;
@@ -5978,109 +5764,10 @@ public class WebView extends AbsoluteLayout
} else if (newScreenY > (newMaxHeight - rectViewY - rectViewHeight)) {
newScreenY = viewHeight - (newMaxHeight - rectViewY);
}
- mZoomCenterY = (oldScreenY * scale - newScreenY * mActualScale)
- / (scale - mActualScale);
- zoomWithPreview(scale, false);
- }
- }
-
- void dismissZoomControl() {
- if (mWebViewCore == null) {
- // maybe called after WebView's destroy(). As we can't get settings,
- // just hide zoom control for both styles.
- if (mZoomButtonsController != null) {
- mZoomButtonsController.setVisible(false);
- }
- if (mZoomControls != null) {
- mZoomControls.hide();
- }
- return;
- }
- WebSettings settings = getSettings();
- if (settings.getBuiltInZoomControls()) {
- if (mZoomButtonsController != null) {
- mZoomButtonsController.setVisible(false);
- }
- } else {
- if (mZoomControlRunnable != null) {
- mPrivateHandler.removeCallbacks(mZoomControlRunnable);
- }
- if (mZoomControls != null) {
- mZoomControls.hide();
- }
- }
- }
-
- // Rule for double tap:
- // 1. if the current scale is not same as the text wrap scale and layout
- // algorithm is NARROW_COLUMNS, fit to column;
- // 2. if the current state is not overview mode, change to overview mode;
- // 3. if the current state is overview mode, change to default scale.
- private void doDoubleTap() {
- if (mWebViewCore.getSettings().getUseWideViewPort() == false) {
- return;
- }
- mZoomCenterX = mLastTouchX;
- mZoomCenterY = mLastTouchY;
- mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX);
- mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY);
- WebSettings settings = getSettings();
- settings.setDoubleTapToastCount(0);
- // remove the zoom control after double tap
- dismissZoomControl();
- ViewManager.ChildView plugin = mViewManager.hitTest(mAnchorX, mAnchorY);
- if (plugin != null) {
- if (isPluginFitOnScreen(plugin)) {
- mInZoomOverview = true;
- // Force the titlebar fully reveal in overview mode
- if (mScrollY < getTitleHeight()) mScrollY = 0;
- zoomWithPreview((float) getViewWidth() / mZoomOverviewWidth,
- true);
- } else {
- mInZoomOverview = false;
- centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height);
- }
- return;
- }
- boolean zoomToDefault = false;
- if ((settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS)
- && (Math.abs(mActualScale - mTextWrapScale) >= MINIMUM_SCALE_INCREMENT)) {
- setNewZoomScale(mActualScale, true, true);
- float overviewScale = (float) getViewWidth() / mZoomOverviewWidth;
- if (Math.abs(mActualScale - overviewScale) < MINIMUM_SCALE_INCREMENT) {
- mInZoomOverview = true;
- }
- } else if (!mInZoomOverview) {
- float newScale = (float) getViewWidth() / mZoomOverviewWidth;
- if (Math.abs(mActualScale - newScale) >= MINIMUM_SCALE_INCREMENT) {
- mInZoomOverview = true;
- // Force the titlebar fully reveal in overview mode
- if (mScrollY < getTitleHeight()) mScrollY = 0;
- zoomWithPreview(newScale, true);
- } else if (Math.abs(mActualScale - mDefaultScale) >= MINIMUM_SCALE_INCREMENT) {
- zoomToDefault = true;
- }
- } else {
- zoomToDefault = true;
- }
- if (zoomToDefault) {
- mInZoomOverview = false;
- int left = nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale);
- if (left != NO_LEFTEDGE) {
- // add a 5pt padding to the left edge.
- int viewLeft = contentToViewX(left < 5 ? 0 : (left - 5))
- - mScrollX;
- // Re-calculate the zoom center so that the new scroll x will be
- // on the left edge.
- if (viewLeft > 0) {
- mZoomCenterX = viewLeft * mDefaultScale
- / (mDefaultScale - mActualScale);
- } else {
- scrollBy(viewLeft, 0);
- mZoomCenterX = 0;
- }
- }
- zoomWithPreview(mDefaultScale, true);
+ float zoomCenterY = (oldScreenY * scale - newScreenY * actualScale)
+ / (scale - actualScale);
+ mZoomManager.setZoomCenter(zoomCenterX, zoomCenterY);
+ mZoomManager.startZoomAnimation(scale, false);
}
}
@@ -6092,6 +5779,9 @@ public class WebView extends AbsoluteLayout
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ // FIXME: If a subwindow is showing find, and the user touches the
+ // background window, it can steal focus.
+ if (mFindIsUp) return false;
boolean result = false;
if (inEditingMode()) {
result = mWebTextView.requestFocus(direction,
@@ -6179,6 +5869,12 @@ public class WebView extends AbsoluteLayout
public boolean requestChildRectangleOnScreen(View child,
Rect rect,
boolean immediate) {
+ // don't scroll while in zoom animation. When it is done, we will adjust
+ // the necessary components (e.g., WebTextView if it is in editing mode)
+ if (mZoomManager.isFixedLengthAnimationInProgress()) {
+ return false;
+ }
+
rect.offset(child.getLeft() - child.getScrollX(),
child.getTop() - child.getScrollY());
@@ -6321,7 +6017,8 @@ public class WebView extends AbsoluteLayout
}
case SWITCH_TO_SHORTPRESS: {
if (mTouchMode == TOUCH_INIT_MODE) {
- if (mPreventDefault != PREVENT_DEFAULT_YES) {
+ if (!getSettings().supportTouchOnly()
+ && mPreventDefault != PREVENT_DEFAULT_YES) {
mTouchMode = TOUCH_SHORTPRESS_START_MODE;
updateSelection();
} else {
@@ -6335,6 +6032,9 @@ public class WebView extends AbsoluteLayout
break;
}
case SWITCH_TO_LONGPRESS: {
+ if (getSettings().supportTouchOnly()) {
+ removeTouchHighlight(false);
+ }
if (inFullScreenMode() || mDeferTouchProcess) {
TouchEventData ted = new TouchEventData();
ted.mAction = WebViewCore.ACTION_LONGPRESS;
@@ -6346,15 +6046,10 @@ public class WebView extends AbsoluteLayout
// simplicity for now, we don't set it.
ted.mMetaState = 0;
ted.mReprocess = mDeferTouchProcess;
- if (mDeferTouchProcess) {
- ted.mViewX = mLastTouchX;
- ted.mViewY = mLastTouchY;
- }
mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
} else if (mPreventDefault != PREVENT_DEFAULT_YES) {
mTouchMode = TOUCH_DONE_MODE;
performLongClick();
- rebuildWebTextView();
}
break;
}
@@ -6387,70 +6082,36 @@ public class WebView extends AbsoluteLayout
spawnContentScrollTo(msg.arg1, msg.arg2);
break;
case UPDATE_ZOOM_RANGE: {
- WebViewCore.RestoreState restoreState
- = (WebViewCore.RestoreState) msg.obj;
+ WebViewCore.ViewState viewState = (WebViewCore.ViewState) msg.obj;
// mScrollX contains the new minPrefWidth
- updateZoomRange(restoreState, getViewWidth(),
- restoreState.mScrollX, false);
+ mZoomManager.updateZoomRange(viewState, getViewWidth(), viewState.mScrollX);
+ break;
+ }
+ case REPLACE_BASE_CONTENT: {
+ nativeReplaceBaseContent(msg.arg1);
break;
}
case NEW_PICTURE_MSG_ID: {
- // If we've previously delayed deleting a root
- // layer, do it now.
- if (mDelayedDeleteRootLayer) {
- mDelayedDeleteRootLayer = false;
- nativeSetRootLayer(0);
- }
- WebSettings settings = mWebViewCore.getSettings();
// called for new content
- final int viewWidth = getViewWidth();
- final WebViewCore.DrawData draw =
- (WebViewCore.DrawData) msg.obj;
+ final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj;
+ nativeSetBaseLayer(draw.mBaseLayer);
final Point viewSize = draw.mViewPoint;
- boolean useWideViewport = settings.getUseWideViewPort();
- WebViewCore.RestoreState restoreState = draw.mRestoreState;
- boolean hasRestoreState = restoreState != null;
- if (hasRestoreState) {
- updateZoomRange(restoreState, viewSize.x,
- draw.mMinPrefWidth, true);
+ WebViewCore.ViewState viewState = draw.mViewState;
+ boolean isPictureAfterFirstLayout = viewState != null;
+ if (isPictureAfterFirstLayout) {
+ // Reset the last sent data here since dealing with new page.
+ mLastWidthSent = 0;
+ mZoomManager.onFirstLayout(draw);
if (!mDrawHistory) {
- mInZoomOverview = false;
-
- if (mInitialScaleInPercent > 0) {
- setNewZoomScale(mInitialScaleInPercent / 100.0f,
- mInitialScaleInPercent != mTextWrapScale * 100,
- false);
- } else if (restoreState.mViewScale > 0) {
- mTextWrapScale = restoreState.mTextWrapScale;
- setNewZoomScale(restoreState.mViewScale, false,
- false);
- } else {
- mInZoomOverview = useWideViewport
- && settings.getLoadWithOverviewMode();
- float scale;
- if (mInZoomOverview) {
- scale = (float) viewWidth
- / DEFAULT_VIEWPORT_WIDTH;
- } else {
- scale = restoreState.mTextWrapScale;
- }
- setNewZoomScale(scale, Math.abs(scale
- - mTextWrapScale) >= MINIMUM_SCALE_INCREMENT,
- false);
- }
- setContentScrollTo(restoreState.mScrollX,
- restoreState.mScrollY);
+ setContentScrollTo(viewState.mScrollX, viewState.mScrollY);
// As we are on a new page, remove the WebTextView. This
// is necessary for page loads driven by webkit, and in
// particular when the user was on a password field, so
// the WebTextView was visible.
clearTextEntry(false);
- // update the zoom buttons as the scale can be changed
- if (getSettings().getBuiltInZoomControls()) {
- updateZoomButtonsEnabled();
- }
}
}
+
// We update the layout (i.e. request a layout from the
// view system) if the last view size that we sent to
// WebCore matches the view size of the picture we just
@@ -6458,45 +6119,25 @@ public class WebView extends AbsoluteLayout
final boolean updateLayout = viewSize.x == mLastWidthSent
&& viewSize.y == mLastHeightSent;
recordNewContentSize(draw.mWidthHeight.x,
- draw.mWidthHeight.y
- + (mFindIsUp ? mFindHeight : 0), updateLayout);
+ draw.mWidthHeight.y, updateLayout);
if (DebugFlags.WEB_VIEW) {
Rect b = draw.mInvalRegion.getBounds();
Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" +
b.left+","+b.top+","+b.right+","+b.bottom+"}");
}
invalidateContentRect(draw.mInvalRegion.getBounds());
+
if (mPictureListener != null) {
mPictureListener.onNewPicture(WebView.this, capturePicture());
}
- if (useWideViewport) {
- // limit mZoomOverviewWidth upper bound to
- // sMaxViewportWidth so that if the page doesn't behave
- // well, the WebView won't go insane. limit the lower
- // bound to match the default scale for mobile sites.
- mZoomOverviewWidth = Math.min(sMaxViewportWidth, Math
- .max((int) (viewWidth / mDefaultScale), Math
- .max(draw.mMinPrefWidth,
- draw.mViewPoint.x)));
- }
- if (!mMinZoomScaleFixed) {
- mMinZoomScale = (float) viewWidth / mZoomOverviewWidth;
- }
- if (!mDrawHistory && mInZoomOverview) {
- // fit the content width to the current view. Ignore
- // the rounding error case.
- if (Math.abs((viewWidth * mInvActualScale)
- - mZoomOverviewWidth) > 1) {
- setNewZoomScale((float) viewWidth
- / mZoomOverviewWidth, Math.abs(mActualScale
- - mTextWrapScale) < MINIMUM_SCALE_INCREMENT,
- false);
- }
- }
+
+ // update the zoom information based on the new picture
+ mZoomManager.onNewPicture(draw);
+
if (draw.mFocusSizeChanged && inEditingMode()) {
mFocusSizeChanged = true;
}
- if (hasRestoreState) {
+ if (isPictureAfterFirstLayout) {
mViewManager.postReadyToDrawAll();
}
break;
@@ -6530,14 +6171,8 @@ public class WebView extends AbsoluteLayout
break;
case REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID:
displaySoftKeyboard(true);
- updateTextSelectionFromMessage(msg.arg1, msg.arg2,
- (WebViewCore.TextSelectionData) msg.obj);
- break;
+ // fall through to UPDATE_TEXT_SELECTION_MSG_ID
case UPDATE_TEXT_SELECTION_MSG_ID:
- // If no textfield was in focus, and the user touched one,
- // causing it to send this message, then WebTextView has not
- // been set up yet. Rebuild it so it can set its selection.
- rebuildWebTextView();
updateTextSelectionFromMessage(msg.arg1, msg.arg2,
(WebViewCore.TextSelectionData) msg.obj);
break;
@@ -6555,7 +6190,7 @@ public class WebView extends AbsoluteLayout
}
}
break;
- case MOVE_OUT_OF_PLUGIN:
+ case UNHANDLED_NAV_KEY:
navHandledKey(msg.arg1, 1, false, 0);
break;
case UPDATE_TEXT_ENTRY_MSG_ID:
@@ -6580,23 +6215,6 @@ public class WebView extends AbsoluteLayout
}
break;
}
- case IMMEDIATE_REPAINT_MSG_ID: {
- invalidate();
- break;
- }
- case SET_ROOT_LAYER_MSG_ID: {
- if (0 == msg.arg1) {
- // Null indicates deleting the old layer, but
- // don't actually do so until we've got the
- // new page to display.
- mDelayedDeleteRootLayer = true;
- } else {
- mDelayedDeleteRootLayer = false;
- nativeSetRootLayer(msg.arg1);
- invalidate();
- }
- break;
- }
case REQUEST_FORM_DATA:
AutoCompleteAdapter adapter = (AutoCompleteAdapter) msg.obj;
if (mWebTextView.isSameTextField(msg.arg1)) {
@@ -6640,33 +6258,40 @@ public class WebView extends AbsoluteLayout
mPreventDefault = msg.arg2 == 1 ? PREVENT_DEFAULT_YES
: PREVENT_DEFAULT_NO;
}
+ if (mPreventDefault == PREVENT_DEFAULT_YES) {
+ mTouchHighlightRegion.setEmpty();
+ }
} else if (msg.arg2 == 0) {
// prevent default is not called in WebCore, so the
// message needs to be reprocessed in UI
TouchEventData ted = (TouchEventData) msg.obj;
switch (ted.mAction) {
case MotionEvent.ACTION_DOWN:
- mLastDeferTouchX = ted.mViewX;
- mLastDeferTouchY = ted.mViewY;
+ mLastDeferTouchX = contentToViewX(ted.mX)
+ - mScrollX;
+ mLastDeferTouchY = contentToViewY(ted.mY)
+ - mScrollY;
mDeferTouchMode = TOUCH_INIT_MODE;
break;
case MotionEvent.ACTION_MOVE: {
// no snapping in defer process
+ int x = contentToViewX(ted.mX) - mScrollX;
+ int y = contentToViewY(ted.mY) - mScrollY;
if (mDeferTouchMode != TOUCH_DRAG_MODE) {
mDeferTouchMode = TOUCH_DRAG_MODE;
- mLastDeferTouchX = ted.mViewX;
- mLastDeferTouchY = ted.mViewY;
+ mLastDeferTouchX = x;
+ mLastDeferTouchY = y;
startDrag();
}
int deltaX = pinLocX((int) (mScrollX
- + mLastDeferTouchX - ted.mViewX))
+ + mLastDeferTouchX - x))
- mScrollX;
int deltaY = pinLocY((int) (mScrollY
- + mLastDeferTouchY - ted.mViewY))
+ + mLastDeferTouchY - y))
- mScrollY;
doDrag(deltaX, deltaY);
- if (deltaX != 0) mLastDeferTouchX = ted.mViewX;
- if (deltaY != 0) mLastDeferTouchY = ted.mViewY;
+ if (deltaX != 0) mLastDeferTouchX = x;
+ if (deltaY != 0) mLastDeferTouchY = y;
break;
}
case MotionEvent.ACTION_UP:
@@ -6680,9 +6305,9 @@ public class WebView extends AbsoluteLayout
break;
case WebViewCore.ACTION_DOUBLETAP:
// doDoubleTap() needs mLastTouchX/Y as anchor
- mLastTouchX = ted.mViewX;
- mLastTouchY = ted.mViewY;
- doDoubleTap();
+ mLastTouchX = contentToViewX(ted.mX) - mScrollX;
+ mLastTouchY = contentToViewY(ted.mY) - mScrollY;
+ mZoomManager.handleDoubleTap(mLastTouchX, mLastTouchY);
mDeferTouchMode = TOUCH_DONE_MODE;
break;
case WebViewCore.ACTION_LONGPRESS:
@@ -6690,7 +6315,6 @@ public class WebView extends AbsoluteLayout
if (hitTest != null && hitTest.mType
!= HitTestResult.UNKNOWN_TYPE) {
performLongClick();
- rebuildWebTextView();
}
mDeferTouchMode = TOUCH_DONE_MODE;
break;
@@ -6814,7 +6438,6 @@ public class WebView extends AbsoluteLayout
case CENTER_FIT_RECT:
Rect r = (Rect)msg.obj;
- mInZoomOverview = false;
centerFitRect(r.left, r.top, r.width(), r.height());
break;
@@ -6823,6 +6446,43 @@ public class WebView extends AbsoluteLayout
mVerticalScrollBarMode = msg.arg2;
break;
+ case SELECTION_STRING_CHANGED:
+ if (mAccessibilityInjector != null) {
+ String selectionString = (String) msg.obj;
+ mAccessibilityInjector.onSelectionStringChange(selectionString);
+ }
+ break;
+
+ case SET_TOUCH_HIGHLIGHT_RECTS:
+ invalidate(mTouchHighlightRegion.getBounds());
+ mTouchHighlightRegion.setEmpty();
+ if (msg.obj != null) {
+ ArrayList<Rect> rects = (ArrayList<Rect>) msg.obj;
+ for (Rect rect : rects) {
+ Rect viewRect = contentToViewRect(rect);
+ // some sites, like stories in nytimes.com, set
+ // mouse event handler in the top div. It is not
+ // user friendly to highlight the div if it covers
+ // more than half of the screen.
+ if (viewRect.width() < getWidth() >> 1
+ || viewRect.height() < getHeight() >> 1) {
+ mTouchHighlightRegion.union(viewRect);
+ invalidate(viewRect);
+ } else {
+ Log.w(LOGTAG, "Skip the huge selection rect:"
+ + viewRect);
+ }
+ }
+ }
+ break;
+
+ case SAVE_WEBARCHIVE_FINISHED:
+ SaveWebArchiveMessage saveMessage = (SaveWebArchiveMessage)msg.obj;
+ if (saveMessage.mCallback != null) {
+ saveMessage.mCallback.onReceiveValue(saveMessage.mResultFile);
+ }
+ break;
+
default:
super.handleMessage(msg);
break;
@@ -7130,37 +6790,6 @@ public class WebView extends AbsoluteLayout
new InvokeListBox(array, enabledArray, selectedArray));
}
- private void updateZoomRange(WebViewCore.RestoreState restoreState,
- int viewWidth, int minPrefWidth, boolean updateZoomOverview) {
- if (restoreState.mMinScale == 0) {
- if (restoreState.mMobileSite) {
- if (minPrefWidth > Math.max(0, viewWidth)) {
- mMinZoomScale = (float) viewWidth / minPrefWidth;
- mMinZoomScaleFixed = false;
- if (updateZoomOverview) {
- WebSettings settings = getSettings();
- mInZoomOverview = settings.getUseWideViewPort() &&
- settings.getLoadWithOverviewMode();
- }
- } else {
- mMinZoomScale = restoreState.mDefaultScale;
- mMinZoomScaleFixed = true;
- }
- } else {
- mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE;
- mMinZoomScaleFixed = false;
- }
- } else {
- mMinZoomScale = restoreState.mMinScale;
- mMinZoomScaleFixed = true;
- }
- if (restoreState.mMaxScale == 0) {
- mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE;
- } else {
- mMaxZoomScale = restoreState.mMaxScale;
- }
- }
-
/*
* Request a dropdown menu for a listbox with single selection or a single
* <select> element.
@@ -7243,7 +6872,7 @@ public class WebView extends AbsoluteLayout
// FIXME the divisor should be retrieved from somewhere
// the closest thing today is hard-coded into ScrollView.java
// (from ScrollView.java, line 363) int maxJump = height/2;
- return Math.round(height * mInvActualScale);
+ return Math.round(height * mZoomManager.getInvScale());
}
/**
@@ -7254,10 +6883,10 @@ public class WebView extends AbsoluteLayout
}
/**
- * Pass the key to the plugin. This assumes that nativeFocusIsPlugin()
- * returned true.
+ * Pass the key directly to the page. This assumes that
+ * nativePageShouldHandleShiftAndArrows() returned true.
*/
- private void letPluginHandleNavKey(int keyCode, long time, boolean down) {
+ private void letPageHandleNavKey(int keyCode, long time, boolean down) {
int keyEventAction;
int eventHubAction;
if (down) {
@@ -7354,7 +6983,7 @@ public class WebView extends AbsoluteLayout
* @hide only needs to be accessible to Browser and testing
*/
public void drawPage(Canvas canvas) {
- mWebViewCore.drawContentPicture(canvas, 0, false, false);
+ nativeDraw(canvas, 0, 0, false);
}
/**
@@ -7398,9 +7027,18 @@ public class WebView extends AbsoluteLayout
private native boolean nativeCursorWantsKeyEvents();
private native void nativeDebugDump();
private native void nativeDestroy();
- private native boolean nativeEvaluateLayersAnimations();
- private native void nativeDrawExtras(Canvas canvas, int extra);
+
+ /**
+ * Draw the picture set with a background color and extra. If
+ * "splitIfNeeded" is true and the return value is not 0, the return value
+ * MUST be passed to WebViewCore with SPLIT_PICTURE_SET message so that the
+ * native allocation can be freed.
+ */
+ private native int nativeDraw(Canvas canvas, int color, int extra,
+ boolean splitIfNeeded);
private native void nativeDumpDisplayTree(String urlOrNull);
+ private native boolean nativeEvaluateLayersAnimations();
+ private native void nativeExtendSelection(int x, int y);
private native int nativeFindAll(String findLower, String findUpper);
private native void nativeFindNext(boolean forward);
/* package */ native int nativeFocusCandidateFramePointer();
@@ -7427,6 +7065,7 @@ public class WebView extends AbsoluteLayout
private native boolean nativeHasCursorNode();
private native boolean nativeHasFocusNode();
private native void nativeHideCursor();
+ private native boolean nativeHitSelection(int x, int y);
private native String nativeImageURI(int x, int y);
private native void nativeInstrumentReport();
/* package */ native boolean nativeMoveCursorToNextTextInput();
@@ -7436,29 +7075,46 @@ public class WebView extends AbsoluteLayout
private native boolean nativeMoveCursor(int keyCode, int count,
boolean noScroll);
private native int nativeMoveGeneration();
- private native void nativeMoveSelection(int x, int y,
- boolean extendSelection);
+ private native void nativeMoveSelection(int x, int y);
+ /**
+ * @return true if the page should get the shift and arrow keys, rather
+ * than select text/navigation.
+ *
+ * If the focus is a plugin, or if the focus and cursor match and are
+ * a contentEditable element, then the page should handle these keys.
+ */
+ private native boolean nativePageShouldHandleShiftAndArrows();
private native boolean nativePointInNavCache(int x, int y, int slop);
// Like many other of our native methods, you must make sure that
// mNativeClass is not null before calling this method.
private native void nativeRecordButtons(boolean focused,
boolean pressed, boolean invalidate);
+ private native void nativeResetSelection();
+ private native void nativeSelectAll();
private native void nativeSelectBestAt(Rect rect);
+ private native int nativeSelectionX();
+ private native int nativeSelectionY();
+ private native int nativeFindIndex();
+ private native void nativeSetExtendSelection();
private native void nativeSetFindIsEmpty();
private native void nativeSetFindIsUp(boolean isUp);
private native void nativeSetFollowedLink(boolean followed);
private native void nativeSetHeightCanMeasure(boolean measure);
- private native void nativeSetRootLayer(int layer);
+ private native void nativeSetBaseLayer(int layer);
+ private native void nativeReplaceBaseContent(int content);
+ private native void nativeCopyBaseContentToPicture(Picture pict);
+ private native boolean nativeHasContent();
private native void nativeSetSelectionPointer(boolean set,
- float scale, int x, int y, boolean extendSelection);
- private native void nativeSetSelectionRegion(boolean set);
+ float scale, int x, int y);
+ private native boolean nativeStartSelection(int x, int y);
private native Rect nativeSubtractLayers(Rect content);
private native int nativeTextGeneration();
// Never call this version except by updateCachedTextfield(String) -
// we always want to pass in our generation number.
private native void nativeUpdateCachedTextfield(String updatedText,
int generation);
+ private native boolean nativeWordSelection(int x, int y);
// return NO_LEFTEDGE means failure.
- private static final int NO_LEFTEDGE = -1;
- private native int nativeGetBlockLeftEdge(int x, int y, float scale);
+ static final int NO_LEFTEDGE = -1;
+ native int nativeGetBlockLeftEdge(int x, int y, float scale);
}
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
index 4118119..21af570 100644
--- a/core/java/android/webkit/WebViewCore.java
+++ b/core/java/android/webkit/WebViewCore.java
@@ -17,14 +17,8 @@
package android.webkit;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
-import android.graphics.Canvas;
-import android.graphics.DrawFilter;
-import android.graphics.Paint;
-import android.graphics.PaintFlagsDrawFilter;
-import android.graphics.Picture;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -33,12 +27,10 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
-import android.provider.Browser;
-import android.provider.OpenableColumns;
+import android.provider.MediaStore;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.KeyEvent;
-import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
@@ -117,7 +109,7 @@ final class WebViewCore {
private int mViewportDensityDpi = -1;
private int mRestoredScale = 0;
- private int mRestoredScreenWidthScale = 0;
+ private int mRestoredTextWrapScale = 0;
private int mRestoredX = 0;
private int mRestoredY = 0;
@@ -279,34 +271,36 @@ final class WebViewCore {
/**
* Called by JNI. Open a file chooser to upload a file.
- * @return String version of the URI plus the name of the file.
- * FIXME: Just return the URI here, and in FileSystem::pathGetFileName, call
- * into Java to get the filename.
+ * @param acceptType The value of the 'accept' attribute of the
+ * input tag associated with this file picker.
+ * @return String version of the URI.
*/
- private String openFileChooser() {
- Uri uri = mCallbackProxy.openFileChooser();
- if (uri == null) return "";
- // Find out the name, and append it to the URI.
- // Webkit will treat the name as the filename, and
- // the URI as the path. The URI will be used
- // in BrowserFrame to get the actual data.
- Cursor cursor = mContext.getContentResolver().query(
- uri,
- new String[] { OpenableColumns.DISPLAY_NAME },
- null,
- null,
- null);
- String name = "";
- if (cursor != null) {
- try {
- if (cursor.moveToNext()) {
- name = cursor.getString(0);
+ private String openFileChooser(String acceptType) {
+ Uri uri = mCallbackProxy.openFileChooser(acceptType);
+ if (uri != null) {
+ String filePath = "";
+ // Note - querying for MediaStore.Images.Media.DATA
+ // seems to work for all content URIs, not just images
+ Cursor cursor = mContext.getContentResolver().query(
+ uri,
+ new String[] { MediaStore.Images.Media.DATA },
+ null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToNext()) {
+ filePath = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
}
- } finally {
- cursor.close();
+ } else {
+ filePath = uri.getLastPathSegment();
}
+ String uriString = uri.toString();
+ BrowserFrame.sJavaBridge.storeFilePathForContentUri(filePath, uriString);
+ return uriString;
}
- return uri.toString() + "/" + name;
+ return "";
}
/**
@@ -424,6 +418,13 @@ final class WebViewCore {
return mCallbackProxy.onJsTimeout();
}
+ /**
+ * Notify the webview that this is an installable web app.
+ */
+ protected void setInstallableWebApp() {
+ mCallbackProxy.setInstallableWebApp();
+ }
+
//-------------------------------------------------------------------------
// JNI methods
//-------------------------------------------------------------------------
@@ -436,35 +437,18 @@ final class WebViewCore {
private native void nativeClearContent();
/**
- * Create a flat picture from the set of pictures.
- */
- private native void nativeCopyContentToPicture(Picture picture);
-
- /**
- * Draw the picture set with a background color. Returns true
- * if some individual picture took too long to draw and can be
- * split into parts. Called from the UI thread.
- */
- private native boolean nativeDrawContent(Canvas canvas, int color);
-
- /**
- * check to see if picture is blank and in progress
- */
- private native boolean nativePictureReady();
-
- /**
* Redraw a portion of the picture set. The Point wh returns the
* width and height of the overall picture.
*/
- private native boolean nativeRecordContent(Region invalRegion, Point wh);
+ private native int nativeRecordContent(Region invalRegion, Point wh);
private native boolean nativeFocusBoundsChanged();
/**
- * Splits slow parts of the picture set. Called from the webkit
- * thread after nativeDrawContent returns true.
+ * Splits slow parts of the picture set. Called from the webkit thread after
+ * WebView.nativeDraw() returns content to be split.
*/
- private native void nativeSplitContent();
+ private native void nativeSplitContent(int content);
private native boolean nativeKey(int keyCode, int unichar,
int repeatCount, boolean isShift, boolean isAlt, boolean isSym,
@@ -480,12 +464,12 @@ final class WebViewCore {
of layout/line-breaking. These coordinates are in document space,
which is the same as View coords unless we have zoomed the document
(see nativeSetZoom).
- screenWidth is used by layout to wrap column around. If viewport uses
- fixed size, screenWidth can be different from width with zooming.
+ textWrapWidth is used by layout to wrap column around. If viewport uses
+ fixed size, textWrapWidth can be different from width with zooming.
should this be called nativeSetViewPortSize?
*/
- private native void nativeSetSize(int width, int height, int screenWidth,
- float scale, int realScreenWidth, int screenHeight, int anchorX,
+ private native void nativeSetSize(int width, int height, int textWrapWidth,
+ float scale, int screenWidth, int screenHeight, int anchorX,
int anchorY, boolean ignoreHeight);
private native int nativeGetContentMinPrefWidth();
@@ -576,7 +560,18 @@ final class WebViewCore {
/**
* Provide WebCore with the previously visted links from the history database
*/
- private native void nativeProvideVisitedHistory(String[] history);
+ private native void nativeProvideVisitedHistory(String[] history);
+
+ /**
+ * Modifies the current selection.
+ *
+ * @param alter Specifies how to alter the selection.
+ * @param direction The direction in which to alter the selection.
+ * @param granularity The granularity of the selection modification.
+ *
+ * @return The selection string.
+ */
+ private native String nativeModifySelection(String alter, String direction, String granularity);
// EventHub for processing messages
private final EventHub mEventHub;
@@ -697,6 +692,12 @@ final class WebViewCore {
int mY;
}
+ static class TouchHighlightData {
+ int mX;
+ int mY;
+ int mSlop;
+ }
+
// mAction of TouchEventData can be MotionEvent.getAction() which uses the
// last two bytes or one of the following values
static final int ACTION_LONGPRESS = 0x100;
@@ -708,8 +709,6 @@ final class WebViewCore {
int mY;
int mMetaState;
boolean mReprocess;
- float mViewX;
- float mViewY;
}
static class GeolocationPermissionsData {
@@ -718,7 +717,11 @@ final class WebViewCore {
boolean mRemember;
}
-
+ static class ModifySelectionData {
+ String mAlter;
+ String mDirection;
+ String mGranularity;
+ }
static final String[] HandlerDebugString = {
"REQUEST_LABEL", // 97
@@ -771,6 +774,7 @@ final class WebViewCore {
"ON_RESUME", // = 144
"FREE_MEMORY", // = 145
"VALID_NODE_BOUNDS", // = 146
+ "SAVE_WEBARCHIVE", // = 147
};
class EventHub {
@@ -837,6 +841,9 @@ final class WebViewCore {
static final int FREE_MEMORY = 145;
static final int VALID_NODE_BOUNDS = 146;
+ // Load and save web archives
+ static final int SAVE_WEBARCHIVE = 147;
+
// Network-based messaging
static final int CLEAR_SSL_PREF_TABLE = 150;
@@ -865,6 +872,12 @@ final class WebViewCore {
static final int ADD_PACKAGE_NAME = 185;
static final int REMOVE_PACKAGE_NAME = 186;
+ static final int GET_TOUCH_HIGHLIGHT_RECTS = 187;
+ static final int REMOVE_TOUCH_HIGHLIGHT_RECTS = 188;
+
+ // accessibility support
+ static final int MODIFY_SELECTION = 190;
+
// private message ids
private static final int DESTROY = 200;
@@ -1238,6 +1251,19 @@ final class WebViewCore {
nativeSetSelection(msg.arg1, msg.arg2);
break;
+ case MODIFY_SELECTION:
+ ModifySelectionData modifySelectionData =
+ (ModifySelectionData) msg.obj;
+ String selectionString = nativeModifySelection(
+ modifySelectionData.mAlter,
+ modifySelectionData.mDirection,
+ modifySelectionData.mGranularity);
+
+ mWebView.mPrivateHandler.obtainMessage(
+ WebView.SELECTION_STRING_CHANGED, selectionString)
+ .sendToTarget();
+ break;
+
case LISTBOX_CHOICES:
SparseBooleanArray choices = (SparseBooleanArray)
msg.obj;
@@ -1278,6 +1304,15 @@ final class WebViewCore {
nativeSetJsFlags((String)msg.obj);
break;
+ case SAVE_WEBARCHIVE:
+ WebView.SaveWebArchiveMessage saveMessage =
+ (WebView.SaveWebArchiveMessage)msg.obj;
+ saveMessage.mResultFile =
+ saveWebArchive(saveMessage.mBasename, saveMessage.mAutoname);
+ mWebView.mPrivateHandler.obtainMessage(
+ WebView.SAVE_WEBARCHIVE_FINISHED, saveMessage).sendToTarget();
+ break;
+
case GEOLOCATION_PERMISSIONS_PROVIDE:
GeolocationPermissionsData data =
(GeolocationPermissionsData) msg.obj;
@@ -1291,7 +1326,9 @@ final class WebViewCore {
break;
case SPLIT_PICTURE_SET:
- nativeSplitContent();
+ nativeSplitContent(msg.arg1);
+ mWebView.mPrivateHandler.obtainMessage(
+ WebView.REPLACE_BASE_CONTENT, msg.arg1, 0);
mSplitPictureIsScheduled = false;
break;
@@ -1357,6 +1394,21 @@ final class WebViewCore {
BrowserFrame.sJavaBridge.removePackageName(
(String) msg.obj);
break;
+
+ case GET_TOUCH_HIGHLIGHT_RECTS:
+ TouchHighlightData d = (TouchHighlightData) msg.obj;
+ ArrayList<Rect> rects = nativeGetTouchHighlightRects
+ (d.mX, d.mY, d.mSlop);
+ mWebView.mPrivateHandler.obtainMessage(
+ WebView.SET_TOUCH_HIGHLIGHT_RECTS, rects)
+ .sendToTarget();
+ break;
+
+ case REMOVE_TOUCH_HIGHLIGHT_RECTS:
+ mWebView.mPrivateHandler.obtainMessage(
+ WebView.SET_TOUCH_HIGHLIGHT_RECTS, null)
+ .sendToTarget();
+ break;
}
}
};
@@ -1562,6 +1614,13 @@ final class WebViewCore {
mBrowserFrame.loadUrl(url, extraHeaders);
}
+ private String saveWebArchive(String filename, boolean autoname) {
+ if (DebugFlags.WEB_VIEW_CORE) {
+ Log.v(LOGTAG, " CORE saveWebArchive " + filename + " " + autoname);
+ }
+ return mBrowserFrame.saveWebArchive(filename, autoname);
+ }
+
private void key(KeyEvent evt, boolean isDown) {
if (DebugFlags.WEB_VIEW_CORE) {
Log.v(LOGTAG, "CORE key at " + System.currentTimeMillis() + ", "
@@ -1575,11 +1634,12 @@ final class WebViewCore {
if (keyCode >= KeyEvent.KEYCODE_DPAD_UP
&& keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) {
if (DebugFlags.WEB_VIEW_CORE) {
- Log.v(LOGTAG, "key: arrow unused by plugin: " + keyCode);
+ Log.v(LOGTAG, "key: arrow unused by page: " + keyCode);
}
if (mWebView != null && evt.isDown()) {
Message.obtain(mWebView.mPrivateHandler,
- WebView.MOVE_OUT_OF_PLUGIN, keyCode).sendToTarget();
+ WebView.UNHANDLED_NAV_KEY, keyCode,
+ 0).sendToTarget();
}
return;
}
@@ -1675,6 +1735,14 @@ final class WebViewCore {
return usedQuota;
}
+ // called from UI thread
+ void splitContent(int content) {
+ if (!mSplitPictureIsScheduled) {
+ mSplitPictureIsScheduled = true;
+ sendMessage(EventHub.SPLIT_PICTURE_SET, content, 0);
+ }
+ }
+
// Used to avoid posting more than one draw message.
private boolean mDrawIsScheduled;
@@ -1684,11 +1752,11 @@ final class WebViewCore {
// Used to suspend drawing.
private boolean mDrawIsPaused;
- // mRestoreState is set in didFirstLayout(), and reset in the next
- // webkitDraw after passing it to the UI thread.
- private RestoreState mRestoreState = null;
+ // mInitialViewState is set by didFirstLayout() and then reset in the
+ // next webkitDraw after passing the state to the UI thread.
+ private ViewState mInitialViewState = null;
- static class RestoreState {
+ static class ViewState {
float mMinScale;
float mMaxScale;
float mViewScale;
@@ -1701,15 +1769,17 @@ final class WebViewCore {
static class DrawData {
DrawData() {
+ mBaseLayer = 0;
mInvalRegion = new Region();
mWidthHeight = new Point();
}
+ int mBaseLayer;
Region mInvalRegion;
Point mViewPoint;
Point mWidthHeight;
int mMinPrefWidth;
- RestoreState mRestoreState; // only non-null if it is for the first
- // picture set after the first layout
+ // only non-null if it is for the first picture set after the first layout
+ ViewState mViewState;
boolean mFocusSizeChanged;
}
@@ -1717,8 +1787,8 @@ final class WebViewCore {
mDrawIsScheduled = false;
DrawData draw = new DrawData();
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start");
- if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight)
- == false) {
+ draw.mBaseLayer = nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight);
+ if (draw.mBaseLayer == 0) {
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort");
return;
}
@@ -1734,9 +1804,9 @@ final class WebViewCore {
: mViewportWidth),
nativeGetContentMinPrefWidth());
}
- if (mRestoreState != null) {
- draw.mRestoreState = mRestoreState;
- mRestoreState = null;
+ if (mInitialViewState != null) {
+ draw.mViewState = mInitialViewState;
+ mInitialViewState = null;
}
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID");
Message.obtain(mWebView.mPrivateHandler,
@@ -1751,51 +1821,6 @@ final class WebViewCore {
}
}
- ///////////////////////////////////////////////////////////////////////////
- // These are called from the UI thread, not our thread
-
- static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG |
- Paint.DITHER_FLAG |
- Paint.SUBPIXEL_TEXT_FLAG;
- static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG |
- Paint.DITHER_FLAG;
-
- final DrawFilter mZoomFilter =
- new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG);
- // If we need to trade better quality for speed, set mScrollFilter to null
- final DrawFilter mScrollFilter =
- new PaintFlagsDrawFilter(SCROLL_BITS, 0);
-
- /* package */ void drawContentPicture(Canvas canvas, int color,
- boolean animatingZoom,
- boolean animatingScroll) {
- DrawFilter df = null;
- if (animatingZoom) {
- df = mZoomFilter;
- } else if (animatingScroll) {
- df = mScrollFilter;
- }
- canvas.setDrawFilter(df);
- boolean tookTooLong = nativeDrawContent(canvas, color);
- canvas.setDrawFilter(null);
- if (tookTooLong && mSplitPictureIsScheduled == false) {
- mSplitPictureIsScheduled = true;
- sendMessage(EventHub.SPLIT_PICTURE_SET);
- }
- }
-
- /* package */ synchronized boolean pictureReady() {
- return 0 != mNativeClass ? nativePictureReady() : false;
- }
-
- /*package*/ synchronized Picture copyContentPicture() {
- Picture result = new Picture();
- if (0 != mNativeClass) {
- nativeCopyContentToPicture(result);
- }
- return result;
- }
-
static void reducePriority() {
// remove the pending REDUCE_PRIORITY and RESUME_PRIORITY messages
sWebCoreHandler.removeMessages(WebCoreThread.REDUCE_PRIORITY);
@@ -1817,6 +1842,8 @@ final class WebViewCore {
// called from UI thread while WEBKIT_DRAW is just pulled out of the
// queue in WebCore thread to be executed. Then update won't be blocked.
if (core != null) {
+ if (!core.getSettings().enableSmoothTransition()) return;
+
synchronized (core) {
core.mDrawIsPaused = true;
if (core.mDrawIsScheduled) {
@@ -1829,6 +1856,10 @@ final class WebViewCore {
static void resumeUpdatePicture(WebViewCore core) {
if (core != null) {
+ // if mDrawIsPaused is true, ignore the setting, continue to resume
+ if (!core.mDrawIsPaused
+ && !core.getSettings().enableSmoothTransition()) return;
+
synchronized (core) {
core.mDrawIsPaused = false;
if (core.mDrawIsScheduled) {
@@ -1971,24 +2002,6 @@ final class WebViewCore {
mRepaintScheduled = false;
}
- // called by JNI
- private void sendImmediateRepaint() {
- if (mWebView != null && !mRepaintScheduled) {
- mRepaintScheduled = true;
- Message.obtain(mWebView.mPrivateHandler,
- WebView.IMMEDIATE_REPAINT_MSG_ID).sendToTarget();
- }
- }
-
- // called by JNI
- private void setRootLayer(int layer) {
- if (mWebView != null) {
- Message.obtain(mWebView.mPrivateHandler,
- WebView.SET_ROOT_LAYER_MSG_ID,
- layer, 0).sendToTarget();
- }
- }
-
/* package */ WebView getWebView() {
return mWebView;
}
@@ -2005,18 +2018,24 @@ final class WebViewCore {
if (mWebView == null) return;
- boolean updateRestoreState = standardLoad || mRestoredScale > 0;
- setupViewport(updateRestoreState);
+ boolean updateViewState = standardLoad || mRestoredScale > 0;
+ setupViewport(updateViewState);
// if updateRestoreState is true, ViewManager.postReadyToDrawAll() will
- // be called after the WebView restore the state. If updateRestoreState
+ // be called after the WebView updates its state. If updateRestoreState
// is false, start to draw now as it is ready.
- if (!updateRestoreState) {
+ if (!updateViewState) {
mWebView.mViewManager.postReadyToDrawAll();
}
+ // remove the touch highlight when moving to a new page
+ if (getSettings().supportTouchOnly()) {
+ mEventHub.sendMessage(Message.obtain(null,
+ EventHub.REMOVE_TOUCH_HIGHLIGHT_RECTS));
+ }
+
// reset the scroll position, the restored offset and scales
mWebkitScrollX = mWebkitScrollY = mRestoredX = mRestoredY
- = mRestoredScale = mRestoredScreenWidthScale = 0;
+ = mRestoredScale = mRestoredTextWrapScale = 0;
}
// called by JNI
@@ -2030,15 +2049,17 @@ final class WebViewCore {
}
}
- private void setupViewport(boolean updateRestoreState) {
+ private void setupViewport(boolean updateViewState) {
// set the viewport settings from WebKit
setViewportSettingsFromNative();
// adjust the default scale to match the densityDpi
float adjust = 1.0f;
if (mViewportDensityDpi == -1) {
- if (WebView.DEFAULT_SCALE_PERCENT != 100) {
- adjust = WebView.DEFAULT_SCALE_PERCENT / 100.0f;
+ // convert default zoom scale to a integer (percentage) to avoid any
+ // issues with floating point comparisons
+ if (mWebView != null && (int)(mWebView.getDefaultZoomScale() * 100) != 100) {
+ adjust = mWebView.getDefaultZoomScale();
}
} else if (mViewportDensityDpi > 0) {
adjust = (float) mContext.getResources().getDisplayMetrics().densityDpi
@@ -2080,17 +2101,17 @@ final class WebViewCore {
}
// if mViewportWidth is 0, it means device-width, always update.
- if (mViewportWidth != 0 && !updateRestoreState) {
- RestoreState restoreState = new RestoreState();
- restoreState.mMinScale = mViewportMinimumScale / 100.0f;
- restoreState.mMaxScale = mViewportMaximumScale / 100.0f;
- restoreState.mDefaultScale = adjust;
+ if (mViewportWidth != 0 && !updateViewState) {
+ ViewState viewState = new ViewState();
+ viewState.mMinScale = mViewportMinimumScale / 100.0f;
+ viewState.mMaxScale = mViewportMaximumScale / 100.0f;
+ viewState.mDefaultScale = adjust;
// as mViewportWidth is not 0, it is not mobile site.
- restoreState.mMobileSite = false;
+ viewState.mMobileSite = false;
// for non-mobile site, we don't need minPrefWidth, set it as 0
- restoreState.mScrollX = 0;
+ viewState.mScrollX = 0;
Message.obtain(mWebView.mPrivateHandler,
- WebView.UPDATE_ZOOM_RANGE, restoreState).sendToTarget();
+ WebView.UPDATE_ZOOM_RANGE, viewState).sendToTarget();
return;
}
@@ -2111,32 +2132,31 @@ final class WebViewCore {
} else {
webViewWidth = Math.round(viewportWidth * mCurrentViewScale);
}
- mRestoreState = new RestoreState();
- mRestoreState.mMinScale = mViewportMinimumScale / 100.0f;
- mRestoreState.mMaxScale = mViewportMaximumScale / 100.0f;
- mRestoreState.mDefaultScale = adjust;
- mRestoreState.mScrollX = mRestoredX;
- mRestoreState.mScrollY = mRestoredY;
- mRestoreState.mMobileSite = (0 == mViewportWidth);
+ mInitialViewState = new ViewState();
+ mInitialViewState.mMinScale = mViewportMinimumScale / 100.0f;
+ mInitialViewState.mMaxScale = mViewportMaximumScale / 100.0f;
+ mInitialViewState.mDefaultScale = adjust;
+ mInitialViewState.mScrollX = mRestoredX;
+ mInitialViewState.mScrollY = mRestoredY;
+ mInitialViewState.mMobileSite = (0 == mViewportWidth);
if (mRestoredScale > 0) {
- mRestoreState.mViewScale = mRestoredScale / 100.0f;
- if (mRestoredScreenWidthScale > 0) {
- mRestoreState.mTextWrapScale =
- mRestoredScreenWidthScale / 100.0f;
+ mInitialViewState.mViewScale = mRestoredScale / 100.0f;
+ if (mRestoredTextWrapScale > 0) {
+ mInitialViewState.mTextWrapScale = mRestoredTextWrapScale / 100.0f;
} else {
- mRestoreState.mTextWrapScale = mRestoreState.mViewScale;
+ mInitialViewState.mTextWrapScale = mInitialViewState.mViewScale;
}
} else {
if (mViewportInitialScale > 0) {
- mRestoreState.mViewScale = mRestoreState.mTextWrapScale =
+ mInitialViewState.mViewScale = mInitialViewState.mTextWrapScale =
mViewportInitialScale / 100.0f;
} else if (mViewportWidth > 0 && mViewportWidth < webViewWidth) {
- mRestoreState.mViewScale = mRestoreState.mTextWrapScale =
+ mInitialViewState.mViewScale = mInitialViewState.mTextWrapScale =
(float) webViewWidth / mViewportWidth;
} else {
- mRestoreState.mTextWrapScale = adjust;
+ mInitialViewState.mTextWrapScale = adjust;
// 0 will trigger WebView to turn on zoom overview mode
- mRestoreState.mViewScale = 0;
+ mInitialViewState.mViewScale = 0;
}
}
@@ -2177,15 +2197,15 @@ final class WebViewCore {
// mViewScale as 0 means it is in zoom overview mode. So we don't
// know the exact scale. If mRestoredScale is non-zero, use it;
// otherwise just use mTextWrapScale as the initial scale.
- data.mScale = mRestoreState.mViewScale == 0
+ data.mScale = mInitialViewState.mViewScale == 0
? (mRestoredScale > 0 ? mRestoredScale / 100.0f
- : mRestoreState.mTextWrapScale)
- : mRestoreState.mViewScale;
+ : mInitialViewState.mTextWrapScale)
+ : mInitialViewState.mViewScale;
if (DebugFlags.WEB_VIEW_CORE) {
Log.v(LOGTAG, "setupViewport"
+ " mRestoredScale=" + mRestoredScale
- + " mViewScale=" + mRestoreState.mViewScale
- + " mTextWrapScale=" + mRestoreState.mTextWrapScale
+ + " mViewScale=" + mInitialViewState.mViewScale
+ + " mTextWrapScale=" + mInitialViewState.mTextWrapScale
);
}
data.mWidth = Math.round(webViewWidth / data.mScale);
@@ -2198,7 +2218,7 @@ final class WebViewCore {
Math.round(mWebView.getViewHeight() / data.mScale)
: mCurrentViewHeight * data.mWidth / viewportWidth;
data.mTextWrapWidth = Math.round(webViewWidth
- / mRestoreState.mTextWrapScale);
+ / mInitialViewState.mTextWrapScale);
data.mIgnoreHeight = false;
data.mAnchorX = data.mAnchorY = 0;
// send VIEW_SIZE_CHANGED to the front of the queue so that we
@@ -2211,20 +2231,12 @@ final class WebViewCore {
}
// called by JNI
- private void restoreScale(int scale) {
+ private void restoreScale(int scale, int textWrapScale) {
if (mBrowserFrame.firstLayoutDone() == false) {
mRestoredScale = scale;
- }
- }
-
- // called by JNI
- private void restoreScreenWidthScale(int scale) {
- if (!mSettings.getUseWideViewPort()) {
- return;
- }
-
- if (mBrowserFrame.firstLayoutDone() == false) {
- mRestoredScreenWidthScale = scale;
+ if (mSettings.getUseWideViewPort()) {
+ mRestoredTextWrapScale = textWrapScale;
+ }
}
}
@@ -2469,4 +2481,6 @@ final class WebViewCore {
private native boolean nativeValidNodeAndBounds(int frame, int node,
Rect bounds);
+ private native ArrayList<Rect> nativeGetTouchHighlightRects(int x, int y,
+ int slop);
}
diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java
index b18419d..d75d421 100644
--- a/core/java/android/webkit/WebViewDatabase.java
+++ b/core/java/android/webkit/WebViewDatabase.java
@@ -173,112 +173,140 @@ public class WebViewDatabase {
private static int mCacheTransactionRefcount;
- private WebViewDatabase() {
+ // Initially true until the background thread completes.
+ private boolean mInitialized = false;
+
+ private WebViewDatabase(final Context context) {
+ new Thread() {
+ @Override
+ public void run() {
+ init(context);
+ }
+ }.start();
+
// Singleton only, use getInstance()
}
public static synchronized WebViewDatabase getInstance(Context context) {
if (mInstance == null) {
- mInstance = new WebViewDatabase();
- try {
- mDatabase = context
- .openOrCreateDatabase(DATABASE_FILE, 0, null);
- } catch (SQLiteException e) {
- // try again by deleting the old db and create a new one
- if (context.deleteDatabase(DATABASE_FILE)) {
- mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0,
- null);
- }
- }
+ mInstance = new WebViewDatabase(context);
+ }
+ return mInstance;
+ }
- // mDatabase should not be null,
- // the only case is RequestAPI test has problem to create db
- if (mDatabase != null && mDatabase.getVersion() != DATABASE_VERSION) {
- mDatabase.beginTransaction();
- try {
- upgradeDatabase();
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- }
+ private synchronized void init(Context context) {
+ if (mInitialized) {
+ return;
+ }
- if (mDatabase != null) {
- // use per table Mutex lock, turn off database lock, this
- // improves performance as database's ReentrantLock is expansive
- mDatabase.setLockingEnabled(false);
+ try {
+ mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null);
+ } catch (SQLiteException e) {
+ // try again by deleting the old db and create a new one
+ if (context.deleteDatabase(DATABASE_FILE)) {
+ mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0,
+ null);
}
+ }
+
+ // mDatabase should not be null,
+ // the only case is RequestAPI test has problem to create db
+ if (mDatabase == null) {
+ mInitialized = true;
+ notify();
+ return;
+ }
+ if (mDatabase.getVersion() != DATABASE_VERSION) {
+ mDatabase.beginTransaction();
try {
- mCacheDatabase = context.openOrCreateDatabase(
- CACHE_DATABASE_FILE, 0, null);
- } catch (SQLiteException e) {
- // try again by deleting the old db and create a new one
- if (context.deleteDatabase(CACHE_DATABASE_FILE)) {
- mCacheDatabase = context.openOrCreateDatabase(
- CACHE_DATABASE_FILE, 0, null);
- }
+ upgradeDatabase();
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
}
+ }
- // mCacheDatabase should not be null,
- // the only case is RequestAPI test has problem to create db
- if (mCacheDatabase != null
- && mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) {
- mCacheDatabase.beginTransaction();
- try {
- upgradeCacheDatabase();
- bootstrapCacheDatabase();
- mCacheDatabase.setTransactionSuccessful();
- } finally {
- mCacheDatabase.endTransaction();
- }
- // Erase the files from the file system in the
- // case that the database was updated and the
- // there were existing cache content
- CacheManager.removeAllCacheFiles();
+ // use per table Mutex lock, turn off database lock, this
+ // improves performance as database's ReentrantLock is
+ // expansive
+ mDatabase.setLockingEnabled(false);
+
+ try {
+ mCacheDatabase = context.openOrCreateDatabase(
+ CACHE_DATABASE_FILE, 0, null);
+ } catch (SQLiteException e) {
+ // try again by deleting the old db and create a new one
+ if (context.deleteDatabase(CACHE_DATABASE_FILE)) {
+ mCacheDatabase = context.openOrCreateDatabase(
+ CACHE_DATABASE_FILE, 0, null);
}
+ }
- if (mCacheDatabase != null) {
- // use read_uncommitted to speed up READ
- mCacheDatabase.execSQL("PRAGMA read_uncommitted = true;");
- // as only READ can be called in the non-WebViewWorkerThread,
- // and read_uncommitted is used, we can turn off database lock
- // to use transaction.
- mCacheDatabase.setLockingEnabled(false);
+ // mCacheDatabase should not be null,
+ // the only case is RequestAPI test has problem to create db
+ if (mCacheDatabase == null) {
+ mInitialized = true;
+ notify();
+ return;
+ }
- // use InsertHelper for faster insertion
- mCacheInserter = new DatabaseUtils.InsertHelper(mCacheDatabase,
- "cache");
- mCacheUrlColIndex = mCacheInserter
- .getColumnIndex(CACHE_URL_COL);
- mCacheFilePathColIndex = mCacheInserter
- .getColumnIndex(CACHE_FILE_PATH_COL);
- mCacheLastModifyColIndex = mCacheInserter
- .getColumnIndex(CACHE_LAST_MODIFY_COL);
- mCacheETagColIndex = mCacheInserter
- .getColumnIndex(CACHE_ETAG_COL);
- mCacheExpiresColIndex = mCacheInserter
- .getColumnIndex(CACHE_EXPIRES_COL);
- mCacheExpiresStringColIndex = mCacheInserter
- .getColumnIndex(CACHE_EXPIRES_STRING_COL);
- mCacheMimeTypeColIndex = mCacheInserter
- .getColumnIndex(CACHE_MIMETYPE_COL);
- mCacheEncodingColIndex = mCacheInserter
- .getColumnIndex(CACHE_ENCODING_COL);
- mCacheHttpStatusColIndex = mCacheInserter
- .getColumnIndex(CACHE_HTTP_STATUS_COL);
- mCacheLocationColIndex = mCacheInserter
- .getColumnIndex(CACHE_LOCATION_COL);
- mCacheContentLengthColIndex = mCacheInserter
- .getColumnIndex(CACHE_CONTENTLENGTH_COL);
- mCacheContentDispositionColIndex = mCacheInserter
- .getColumnIndex(CACHE_CONTENTDISPOSITION_COL);
- mCacheCrossDomainColIndex = mCacheInserter
- .getColumnIndex(CACHE_CROSSDOMAIN_COL);
+ if (mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) {
+ mCacheDatabase.beginTransaction();
+ try {
+ upgradeCacheDatabase();
+ bootstrapCacheDatabase();
+ mCacheDatabase.setTransactionSuccessful();
+ } finally {
+ mCacheDatabase.endTransaction();
}
+ // Erase the files from the file system in the
+ // case that the database was updated and the
+ // there were existing cache content
+ CacheManager.removeAllCacheFiles();
}
- return mInstance;
+ // use read_uncommitted to speed up READ
+ mCacheDatabase.execSQL("PRAGMA read_uncommitted = true;");
+ // as only READ can be called in the
+ // non-WebViewWorkerThread, and read_uncommitted is used,
+ // we can turn off database lock to use transaction.
+ mCacheDatabase.setLockingEnabled(false);
+
+ // use InsertHelper for faster insertion
+ mCacheInserter =
+ new DatabaseUtils.InsertHelper(mCacheDatabase,
+ "cache");
+ mCacheUrlColIndex = mCacheInserter
+ .getColumnIndex(CACHE_URL_COL);
+ mCacheFilePathColIndex = mCacheInserter
+ .getColumnIndex(CACHE_FILE_PATH_COL);
+ mCacheLastModifyColIndex = mCacheInserter
+ .getColumnIndex(CACHE_LAST_MODIFY_COL);
+ mCacheETagColIndex = mCacheInserter
+ .getColumnIndex(CACHE_ETAG_COL);
+ mCacheExpiresColIndex = mCacheInserter
+ .getColumnIndex(CACHE_EXPIRES_COL);
+ mCacheExpiresStringColIndex = mCacheInserter
+ .getColumnIndex(CACHE_EXPIRES_STRING_COL);
+ mCacheMimeTypeColIndex = mCacheInserter
+ .getColumnIndex(CACHE_MIMETYPE_COL);
+ mCacheEncodingColIndex = mCacheInserter
+ .getColumnIndex(CACHE_ENCODING_COL);
+ mCacheHttpStatusColIndex = mCacheInserter
+ .getColumnIndex(CACHE_HTTP_STATUS_COL);
+ mCacheLocationColIndex = mCacheInserter
+ .getColumnIndex(CACHE_LOCATION_COL);
+ mCacheContentLengthColIndex = mCacheInserter
+ .getColumnIndex(CACHE_CONTENTLENGTH_COL);
+ mCacheContentDispositionColIndex = mCacheInserter
+ .getColumnIndex(CACHE_CONTENTDISPOSITION_COL);
+ mCacheCrossDomainColIndex = mCacheInserter
+ .getColumnIndex(CACHE_CROSSDOMAIN_COL);
+
+ // Thread done, notify.
+ mInitialized = true;
+ notify();
}
private static void upgradeDatabase() {
@@ -391,8 +419,25 @@ public class WebViewDatabase {
}
}
+ // Wait for the background initialization thread to complete and check the
+ // database creation status.
+ private boolean checkInitialized() {
+ synchronized (this) {
+ while (!mInitialized) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while checking " +
+ "initialization");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ }
+ return mDatabase != null;
+ }
+
private boolean hasEntries(int tableId) {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return false;
}
@@ -422,7 +467,7 @@ public class WebViewDatabase {
*/
ArrayList<Cookie> getCookiesForDomain(String domain) {
ArrayList<Cookie> list = new ArrayList<Cookie>();
- if (domain == null || mDatabase == null) {
+ if (domain == null || !checkInitialized()) {
return list;
}
@@ -481,7 +526,7 @@ public class WebViewDatabase {
* deleted.
*/
void deleteCookies(String domain, String path, String name) {
- if (domain == null || mDatabase == null) {
+ if (domain == null || !checkInitialized()) {
return;
}
@@ -501,7 +546,7 @@ public class WebViewDatabase {
*/
void addCookie(Cookie cookie) {
if (cookie.domain == null || cookie.path == null || cookie.name == null
- || mDatabase == null) {
+ || !checkInitialized()) {
return;
}
@@ -534,7 +579,7 @@ public class WebViewDatabase {
* Clear cookie database
*/
void clearCookies() {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return;
}
@@ -547,7 +592,7 @@ public class WebViewDatabase {
* Clear session cookies, which means cookie doesn't have EXPIRES.
*/
void clearSessionCookies() {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return;
}
@@ -564,7 +609,7 @@ public class WebViewDatabase {
* @param now Time for now
*/
void clearExpiredCookies(long now) {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return;
}
@@ -620,7 +665,7 @@ public class WebViewDatabase {
* @return CacheResult The CacheManager.CacheResult
*/
CacheResult getCache(String url) {
- if (url == null || mCacheDatabase == null) {
+ if (url == null || !checkInitialized()) {
return null;
}
@@ -660,7 +705,7 @@ public class WebViewDatabase {
* @param url The url
*/
void removeCache(String url) {
- if (url == null || mCacheDatabase == null) {
+ if (url == null || !checkInitialized()) {
return;
}
@@ -674,7 +719,7 @@ public class WebViewDatabase {
* @param c The CacheManager.CacheResult
*/
void addCache(String url, CacheResult c) {
- if (url == null || mCacheDatabase == null) {
+ if (url == null || !checkInitialized()) {
return;
}
@@ -700,7 +745,7 @@ public class WebViewDatabase {
* Clear cache database
*/
void clearCache() {
- if (mCacheDatabase == null) {
+ if (!checkInitialized()) {
return;
}
@@ -708,7 +753,7 @@ public class WebViewDatabase {
}
boolean hasCache() {
- if (mCacheDatabase == null) {
+ if (!checkInitialized()) {
return false;
}
@@ -831,7 +876,7 @@ public class WebViewDatabase {
*/
void setUsernamePassword(String schemePlusHost, String username,
String password) {
- if (schemePlusHost == null || mDatabase == null) {
+ if (schemePlusHost == null || !checkInitialized()) {
return;
}
@@ -853,7 +898,7 @@ public class WebViewDatabase {
* String[1] is password. Return null if it can't find anything.
*/
String[] getUsernamePassword(String schemePlusHost) {
- if (schemePlusHost == null || mDatabase == null) {
+ if (schemePlusHost == null || !checkInitialized()) {
return null;
}
@@ -899,7 +944,7 @@ public class WebViewDatabase {
* Clear password database
*/
public void clearUsernamePassword() {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return;
}
@@ -924,7 +969,7 @@ public class WebViewDatabase {
*/
void setHttpAuthUsernamePassword(String host, String realm, String username,
String password) {
- if (host == null || realm == null || mDatabase == null) {
+ if (host == null || realm == null || !checkInitialized()) {
return;
}
@@ -949,7 +994,7 @@ public class WebViewDatabase {
* String[1] is password. Return null if it can't find anything.
*/
String[] getHttpAuthUsernamePassword(String host, String realm) {
- if (host == null || realm == null || mDatabase == null){
+ if (host == null || realm == null || !checkInitialized()){
return null;
}
@@ -996,7 +1041,7 @@ public class WebViewDatabase {
* Clear HTTP authentication password database
*/
public void clearHttpAuthUsernamePassword() {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return;
}
@@ -1017,7 +1062,7 @@ public class WebViewDatabase {
* @param formdata The form data in HashMap
*/
void setFormData(String url, HashMap<String, String> formdata) {
- if (url == null || formdata == null || mDatabase == null) {
+ if (url == null || formdata == null || !checkInitialized()) {
return;
}
@@ -1066,7 +1111,7 @@ public class WebViewDatabase {
*/
ArrayList<String> getFormData(String url, String name) {
ArrayList<String> values = new ArrayList<String>();
- if (url == null || name == null || mDatabase == null) {
+ if (url == null || name == null || !checkInitialized()) {
return values;
}
@@ -1126,7 +1171,7 @@ public class WebViewDatabase {
* Clear form database
*/
public void clearFormData() {
- if (mDatabase == null) {
+ if (!checkInitialized()) {
return;
}
diff --git a/core/java/android/webkit/ZoomControlBase.java b/core/java/android/webkit/ZoomControlBase.java
new file mode 100644
index 0000000..be9e8f3
--- /dev/null
+++ b/core/java/android/webkit/ZoomControlBase.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.webkit;
+
+interface ZoomControlBase {
+
+ /**
+ * Causes the on-screen zoom control to be made visible
+ */
+ public void show();
+
+ /**
+ * Causes the on-screen zoom control to disappear
+ */
+ public void hide();
+
+ /**
+ * Enables the control to update its state if necessary in response to a
+ * change in the pages zoom level. For example, if the max zoom level is
+ * reached then the control can disable the button for zooming in.
+ */
+ public void update();
+
+ /**
+ * Checks to see if the control is currently visible to the user.
+ */
+ public boolean isVisible();
+}
diff --git a/core/java/android/webkit/ZoomControlEmbedded.java b/core/java/android/webkit/ZoomControlEmbedded.java
new file mode 100644
index 0000000..c29e72b
--- /dev/null
+++ b/core/java/android/webkit/ZoomControlEmbedded.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.webkit;
+
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.Toast;
+import android.widget.ZoomButtonsController;
+
+class ZoomControlEmbedded implements ZoomControlBase {
+
+ private final ZoomManager mZoomManager;
+ private final WebView mWebView;
+
+ // The controller is lazily initialized in getControls() for performance.
+ private ZoomButtonsController mZoomButtonsController;
+
+ public ZoomControlEmbedded(ZoomManager zoomManager, WebView webView) {
+ mZoomManager = zoomManager;
+ mWebView = webView;
+ }
+
+ public void show() {
+ if (!getControls().isVisible() && !mZoomManager.isZoomScaleFixed()) {
+
+ mZoomButtonsController.setVisible(true);
+
+ WebSettings settings = mWebView.getSettings();
+ int count = settings.getDoubleTapToastCount();
+ if (mZoomManager.isInZoomOverview() && count > 0) {
+ settings.setDoubleTapToastCount(--count);
+ Toast.makeText(mWebView.getContext(),
+ com.android.internal.R.string.double_tap_toast,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ public void hide() {
+ if (mZoomButtonsController != null) {
+ mZoomButtonsController.setVisible(false);
+ }
+ }
+
+ public boolean isVisible() {
+ return mZoomButtonsController != null && mZoomButtonsController.isVisible();
+ }
+
+ public void update() {
+ if (mZoomButtonsController == null) {
+ return;
+ }
+
+ boolean canZoomIn = mZoomManager.canZoomIn();
+ boolean canZoomOut = mZoomManager.canZoomOut() && !mZoomManager.isInZoomOverview();
+ if (!canZoomIn && !canZoomOut) {
+ // Hide the zoom in and out buttons if the page cannot zoom
+ mZoomButtonsController.getZoomControls().setVisibility(View.GONE);
+ } else {
+ // Set each one individually, as a page may be able to zoom in or out
+ mZoomButtonsController.setZoomInEnabled(canZoomIn);
+ mZoomButtonsController.setZoomOutEnabled(canZoomOut);
+ }
+ }
+
+ private ZoomButtonsController getControls() {
+ if (mZoomButtonsController == null) {
+ mZoomButtonsController = new ZoomButtonsController(mWebView);
+ mZoomButtonsController.setOnZoomListener(new ZoomListener());
+ // ZoomButtonsController positions the buttons at the bottom, but in
+ // the middle. Change their layout parameters so they appear on the
+ // right.
+ View controls = mZoomButtonsController.getZoomControls();
+ ViewGroup.LayoutParams params = controls.getLayoutParams();
+ if (params instanceof FrameLayout.LayoutParams) {
+ ((FrameLayout.LayoutParams) params).gravity = Gravity.RIGHT;
+ }
+ }
+ return mZoomButtonsController;
+ }
+
+ private class ZoomListener implements ZoomButtonsController.OnZoomListener {
+
+ public void onVisibilityChanged(boolean visible) {
+ if (visible) {
+ mWebView.switchOutDrawHistory();
+ // Bring back the hidden zoom controls.
+ mZoomButtonsController.getZoomControls().setVisibility(View.VISIBLE);
+ update();
+ }
+ }
+
+ public void onZoom(boolean zoomIn) {
+ if (zoomIn) {
+ mWebView.zoomIn();
+ } else {
+ mWebView.zoomOut();
+ }
+ update();
+ }
+ }
+}
diff --git a/core/java/android/webkit/ZoomControlExternal.java b/core/java/android/webkit/ZoomControlExternal.java
new file mode 100644
index 0000000..d75313e
--- /dev/null
+++ b/core/java/android/webkit/ZoomControlExternal.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.webkit;
+
+import android.content.Context;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.View.OnClickListener;
+import android.view.animation.AlphaAnimation;
+import android.widget.FrameLayout;
+
+@Deprecated
+class ZoomControlExternal implements ZoomControlBase {
+
+ // The time that the external controls are visible before fading away
+ private static final long ZOOM_CONTROLS_TIMEOUT =
+ ViewConfiguration.getZoomControlsTimeout();
+ // The view containing the external zoom controls
+ private ExtendedZoomControls mZoomControls;
+ private Runnable mZoomControlRunnable;
+ private final Handler mPrivateHandler = new Handler();
+
+ private final WebView mWebView;
+
+ public ZoomControlExternal(WebView webView) {
+ mWebView = webView;
+ }
+
+ public void show() {
+ if(mZoomControlRunnable != null) {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ }
+ getControls().show(true);
+ mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT);
+ }
+
+ public void hide() {
+ if (mZoomControlRunnable != null) {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ }
+ if (mZoomControls != null) {
+ mZoomControls.hide();
+ }
+ }
+
+ public boolean isVisible() {
+ return mZoomControls != null && mZoomControls.isShown();
+ }
+
+ public void update() { }
+
+ public ExtendedZoomControls getControls() {
+ if (mZoomControls == null) {
+ mZoomControls = createZoomControls();
+
+ /*
+ * need to be set to VISIBLE first so that getMeasuredHeight() in
+ * {@link #onSizeChanged()} can return the measured value for proper
+ * layout.
+ */
+ mZoomControls.setVisibility(View.VISIBLE);
+ mZoomControlRunnable = new Runnable() {
+ public void run() {
+ /* Don't dismiss the controls if the user has
+ * focus on them. Wait and check again later.
+ */
+ if (!mZoomControls.hasFocus()) {
+ mZoomControls.hide();
+ } else {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ }
+ }
+ };
+ }
+ return mZoomControls;
+ }
+
+ private ExtendedZoomControls createZoomControls() {
+ ExtendedZoomControls zoomControls = new ExtendedZoomControls(mWebView.getContext());
+ zoomControls.setOnZoomInClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ // reset time out
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT);
+ mWebView.zoomIn();
+ }
+ });
+ zoomControls.setOnZoomOutClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ // reset time out
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT);
+ mWebView.zoomOut();
+ }
+ });
+ return zoomControls;
+ }
+
+ private static class ExtendedZoomControls extends FrameLayout {
+
+ private android.widget.ZoomControls mPlusMinusZoomControls;
+
+ public ExtendedZoomControls(Context context) {
+ super(context, null);
+ LayoutInflater inflater = (LayoutInflater)
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true);
+ mPlusMinusZoomControls = (android.widget.ZoomControls) findViewById(
+ com.android.internal.R.id.zoomControls);
+ findViewById(com.android.internal.R.id.zoomMagnify).setVisibility(
+ View.GONE);
+ }
+
+ public void show(boolean showZoom) {
+ mPlusMinusZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE);
+ fade(View.VISIBLE, 0.0f, 1.0f);
+ }
+
+ public void hide() {
+ fade(View.GONE, 1.0f, 0.0f);
+ }
+
+ private void fade(int visibility, float startAlpha, float endAlpha) {
+ AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha);
+ anim.setDuration(500);
+ startAnimation(anim);
+ setVisibility(visibility);
+ }
+
+ public boolean hasFocus() {
+ return mPlusMinusZoomControls.hasFocus();
+ }
+
+ public void setOnZoomInClickListener(OnClickListener listener) {
+ mPlusMinusZoomControls.setOnZoomInClickListener(listener);
+ }
+
+ public void setOnZoomOutClickListener(OnClickListener listener) {
+ mPlusMinusZoomControls.setOnZoomOutClickListener(listener);
+ }
+ }
+}
diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java
new file mode 100644
index 0000000..7f7f46e
--- /dev/null
+++ b/core/java/android/webkit/ZoomManager.java
@@ -0,0 +1,902 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+
+/**
+ * The ZoomManager is responsible for maintaining the WebView's current zoom
+ * level state. It is also responsible for managing the on-screen zoom controls
+ * as well as any animation of the WebView due to zooming.
+ *
+ * Currently, there are two methods for animating the zoom of a WebView.
+ *
+ * (1) The first method is triggered by startZoomAnimation(...) and is a fixed
+ * length animation where the final zoom scale is known at startup. This type of
+ * animation notifies webkit of the final scale BEFORE it animates. The animation
+ * is then done by scaling the CANVAS incrementally based on a stepping function.
+ *
+ * (2) The second method is triggered by a multi-touch pinch and the new scale
+ * is determined dynamically based on the user's gesture. This type of animation
+ * only notifies webkit of new scale AFTER the gesture is complete. The animation
+ * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView)
+ * to the new scale in response to events related to the user's gesture.
+ */
+class ZoomManager {
+
+ static final String LOGTAG = "webviewZoom";
+
+ private final WebView mWebView;
+ private final CallbackProxy mCallbackProxy;
+
+ // Widgets responsible for the on-screen zoom functions of the WebView.
+ private ZoomControlEmbedded mEmbeddedZoomControl;
+ private ZoomControlExternal mExternalZoomControl;
+
+ /*
+ * The scale factors that determine the upper and lower bounds for the
+ * default zoom scale.
+ */
+ protected static final float DEFAULT_MAX_ZOOM_SCALE_FACTOR = 4.00f;
+ protected static final float DEFAULT_MIN_ZOOM_SCALE_FACTOR = 0.25f;
+
+ // The default scale limits, which are dependent on the display density.
+ private float mDefaultMaxZoomScale;
+ private float mDefaultMinZoomScale;
+
+ // The actual scale limits, which can be set through a webpage's viewport
+ // meta-tag.
+ private float mMaxZoomScale;
+ private float mMinZoomScale;
+
+ // Locks the minimum ZoomScale to the value currently set in mMinZoomScale.
+ private boolean mMinZoomScaleFixed = true;
+
+ /*
+ * When loading a new page the WebView does not initially know the final
+ * width of the page. Therefore, when a new page is loaded in overview mode
+ * the overview scale is initialized to a default value. This flag is then
+ * set and used to notify the ZoomManager to take the width of the next
+ * picture from webkit and use that width to enter into zoom overview mode.
+ */
+ private boolean mInitialZoomOverview = false;
+
+ /*
+ * When in the zoom overview mode, the page's width is fully fit to the
+ * current window. Additionally while the page is in this state it is
+ * active, in other words, you can click to follow the links. We cache a
+ * boolean to enable us to quickly check whether or not we are in overview
+ * mode, but this value should only be modified by changes to the zoom
+ * scale.
+ */
+ private boolean mInZoomOverview = false;
+ private int mZoomOverviewWidth;
+ private float mInvZoomOverviewWidth;
+
+ /*
+ * These variables track the center point of the zoom and they are used to
+ * determine the point around which we should zoom. They are stored in view
+ * coordinates.
+ */
+ private float mZoomCenterX;
+ private float mZoomCenterY;
+
+ /*
+ * These values represent the point around which the screen should be
+ * centered after zooming. In other words it is used to determine the center
+ * point of the visible document after the page has finished zooming. This
+ * is important because the zoom may have potentially reflowed the text and
+ * we need to ensure the proper portion of the document remains on the
+ * screen.
+ */
+ private int mAnchorX;
+ private int mAnchorY;
+
+ // The scale factor that is used to determine the column width for text
+ private float mTextWrapScale;
+
+ /*
+ * The default zoom scale is the scale factor used when the user triggers a
+ * zoom in by double tapping on the WebView. The value is initially set
+ * based on the display density, but can be changed at any time via the
+ * WebSettings.
+ */
+ private float mDefaultScale;
+ private float mInvDefaultScale;
+
+ // the current computed zoom scale and its inverse.
+ private float mActualScale;
+ private float mInvActualScale;
+
+ /*
+ * The initial scale for the WebView. 0 means default. If initial scale is
+ * greater than 0 the WebView starts with this value as its initial scale. The
+ * value is converted from an integer percentage so it is guarenteed to have
+ * no more than 2 significant digits after the decimal. This restriction
+ * allows us to convert the scale back to the original percentage by simply
+ * multiplying the value by 100.
+ */
+ private float mInitialScale;
+
+ private static float MINIMUM_SCALE_INCREMENT = 0.01f;
+
+ /*
+ * The following member variables are only to be used for animating zoom. If
+ * mZoomScale is non-zero then we are in the middle of a zoom animation. The
+ * other variables are used as a cache (e.g. inverse) or as a way to store
+ * the state of the view prior to animating (e.g. initial scroll coords).
+ */
+ private float mZoomScale;
+ private float mInvInitialZoomScale;
+ private float mInvFinalZoomScale;
+ private int mInitialScrollX;
+ private int mInitialScrollY;
+ private long mZoomStart;
+
+ private static final int ZOOM_ANIMATION_LENGTH = 500;
+
+ // whether support multi-touch
+ private boolean mSupportMultiTouch;
+
+ // use the framework's ScaleGestureDetector to handle multi-touch
+ private ScaleGestureDetector mScaleDetector;
+ private boolean mPinchToZoomAnimating = false;
+
+ public ZoomManager(WebView webView, CallbackProxy callbackProxy) {
+ mWebView = webView;
+ mCallbackProxy = callbackProxy;
+
+ /*
+ * Ideally mZoomOverviewWidth should be mContentWidth. But sites like
+ * ESPN and Engadget always have wider mContentWidth no matter what the
+ * viewport size is.
+ */
+ setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH);
+ }
+
+ /**
+ * Initialize both the default and actual zoom scale to the given density.
+ *
+ * @param density The logical density of the display. This is a scaling factor
+ * for the Density Independent Pixel unit, where one DIP is one pixel on an
+ * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
+ */
+ public void init(float density) {
+ assert density > 0;
+
+ setDefaultZoomScale(density);
+ mActualScale = density;
+ mInvActualScale = 1 / density;
+ mTextWrapScale = density;
+ }
+
+ /**
+ * Update the default zoom scale using the given density. It will also reset
+ * the current min and max zoom scales to the default boundaries as well as
+ * ensure that the actual scale falls within those boundaries.
+ *
+ * @param density The logical density of the display. This is a scaling factor
+ * for the Density Independent Pixel unit, where one DIP is one pixel on an
+ * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
+ */
+ public void updateDefaultZoomDensity(float density) {
+ assert density > 0;
+
+ if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) {
+ // set the new default density
+ setDefaultZoomScale(density);
+ // adjust the scale if it falls outside the new zoom bounds
+ setZoomScale(mActualScale, true);
+ }
+ }
+
+ private void setDefaultZoomScale(float defaultScale) {
+ mDefaultScale = defaultScale;
+ mInvDefaultScale = 1 / defaultScale;
+ mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR;
+ mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR;
+ mMaxZoomScale = mDefaultMaxZoomScale;
+ mMinZoomScale = mDefaultMinZoomScale;
+ }
+
+ public final float getScale() {
+ return mActualScale;
+ }
+
+ public final float getInvScale() {
+ return mInvActualScale;
+ }
+
+ public final float getTextWrapScale() {
+ return mTextWrapScale;
+ }
+
+ public final float getMaxZoomScale() {
+ return mMaxZoomScale;
+ }
+
+ public final float getMinZoomScale() {
+ return mMinZoomScale;
+ }
+
+ public final float getDefaultScale() {
+ return mDefaultScale;
+ }
+
+ public final float getInvDefaultScale() {
+ return mInvDefaultScale;
+ }
+
+ public final float getDefaultMaxZoomScale() {
+ return mDefaultMaxZoomScale;
+ }
+
+ public final float getDefaultMinZoomScale() {
+ return mDefaultMinZoomScale;
+ }
+
+ public final int getDocumentAnchorX() {
+ return mAnchorX;
+ }
+
+ public final int getDocumentAnchorY() {
+ return mAnchorY;
+ }
+
+ public final void clearDocumentAnchor() {
+ mAnchorX = mAnchorY = 0;
+ }
+
+ public final void setZoomCenter(float x, float y) {
+ mZoomCenterX = x;
+ mZoomCenterY = y;
+ }
+
+ public final void setInitialScaleInPercent(int scaleInPercent) {
+ mInitialScale = scaleInPercent * 0.01f;
+ }
+
+ public final float computeScaleWithLimits(float scale) {
+ if (scale < mMinZoomScale) {
+ scale = mMinZoomScale;
+ } else if (scale > mMaxZoomScale) {
+ scale = mMaxZoomScale;
+ }
+ return scale;
+ }
+
+ public final boolean isZoomScaleFixed() {
+ return mMinZoomScale >= mMaxZoomScale;
+ }
+
+ public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) {
+ return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT;
+ }
+
+ public boolean willScaleTriggerZoom(float scale) {
+ return exceedsMinScaleIncrement(scale, mActualScale);
+ }
+
+ public final boolean canZoomIn() {
+ return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT;
+ }
+
+ public final boolean canZoomOut() {
+ return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT;
+ }
+
+ public boolean zoomIn() {
+ return zoom(1.25f);
+ }
+
+ public boolean zoomOut() {
+ return zoom(0.8f);
+ }
+
+ // returns TRUE if zoom out succeeds and FALSE if no zoom changes.
+ private boolean zoom(float zoomMultiplier) {
+ // TODO: alternatively we can disallow this during draw history mode
+ mWebView.switchOutDrawHistory();
+ // Center zooming to the center of the screen.
+ mZoomCenterX = mWebView.getViewWidth() * .5f;
+ mZoomCenterY = mWebView.getViewHeight() * .5f;
+ mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
+ mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
+ return startZoomAnimation(mActualScale * zoomMultiplier, true);
+ }
+
+ /**
+ * Initiates an animated zoom of the WebView.
+ *
+ * @return true if the new scale triggered an animation and false otherwise.
+ */
+ public boolean startZoomAnimation(float scale, boolean reflowText) {
+ float oldScale = mActualScale;
+ mInitialScrollX = mWebView.getScrollX();
+ mInitialScrollY = mWebView.getScrollY();
+
+ // snap to DEFAULT_SCALE if it is close
+ if (!exceedsMinScaleIncrement(scale, mDefaultScale)) {
+ scale = mDefaultScale;
+ }
+
+ setZoomScale(scale, reflowText);
+
+ if (oldScale != mActualScale) {
+ // use mZoomPickerScale to see zoom preview first
+ mZoomStart = SystemClock.uptimeMillis();
+ mInvInitialZoomScale = 1.0f / oldScale;
+ mInvFinalZoomScale = 1.0f / mActualScale;
+ mZoomScale = mActualScale;
+ mWebView.onFixedLengthZoomAnimationStart();
+ mWebView.invalidate();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * This method is called by the WebView's drawing code when a fixed length zoom
+ * animation is occurring. Its purpose is to animate the zooming of the canvas
+ * to the desired scale which was specified in startZoomAnimation(...).
+ *
+ * A fixed length animation begins when startZoomAnimation(...) is called and
+ * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that
+ * interval each time the WebView draws it calls this function which is
+ * responsible for generating the animation.
+ *
+ * Additionally, the WebView can check to see if such an animation is currently
+ * in progress by calling isFixedLengthAnimationInProgress().
+ */
+ public void animateZoom(Canvas canvas) {
+ if (mZoomScale == 0) {
+ Log.w(LOGTAG, "A WebView is attempting to perform a fixed length "
+ + "zoom animation when no zoom is in progress");
+ return;
+ }
+
+ float zoomScale;
+ int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
+ if (interval < ZOOM_ANIMATION_LENGTH) {
+ float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
+ zoomScale = 1.0f / (mInvInitialZoomScale
+ + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
+ mWebView.invalidate();
+ } else {
+ zoomScale = mZoomScale;
+ // set mZoomScale to be 0 as we have finished animating
+ mZoomScale = 0;
+ mWebView.onFixedLengthZoomAnimationEnd();
+ }
+ // calculate the intermediate scroll position. Since we need to use
+ // zoomScale, we can't use the WebView's pinLocX/Y functions directly.
+ float scale = zoomScale * mInvInitialZoomScale;
+ int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX);
+ tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth()
+ * zoomScale)) + mWebView.getScrollX();
+ int titleHeight = mWebView.getTitleHeight();
+ int ty = Math.round(scale
+ * (mInitialScrollY + mZoomCenterY - titleHeight)
+ - (mZoomCenterY - titleHeight));
+ ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty
+ - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight()
+ * zoomScale)) + titleHeight) + mWebView.getScrollY();
+
+ canvas.translate(tx, ty);
+ canvas.scale(zoomScale, zoomScale);
+ }
+
+ public boolean isZoomAnimating() {
+ return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating;
+ }
+
+ public boolean isFixedLengthAnimationInProgress() {
+ return mZoomScale != 0;
+ }
+
+ public void refreshZoomScale(boolean reflowText) {
+ setZoomScale(mActualScale, reflowText, true);
+ }
+
+ public void setZoomScale(float scale, boolean reflowText) {
+ setZoomScale(scale, reflowText, false);
+ }
+
+ private void setZoomScale(float scale, boolean reflowText, boolean force) {
+ final boolean isScaleLessThanMinZoom = scale < mMinZoomScale;
+ scale = computeScaleWithLimits(scale);
+
+ // determine whether or not we are in the zoom overview mode
+ if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) {
+ mInZoomOverview = true;
+ } else {
+ mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale());
+ }
+
+ if (reflowText) {
+ mTextWrapScale = scale;
+ }
+
+ if (scale != mActualScale || force) {
+ float oldScale = mActualScale;
+ float oldInvScale = mInvActualScale;
+
+ if (scale != mActualScale && !mPinchToZoomAnimating) {
+ mCallbackProxy.onScaleChanged(mActualScale, scale);
+ }
+
+ mActualScale = scale;
+ mInvActualScale = 1 / scale;
+
+ if (!mWebView.drawHistory()) {
+
+ // If history Picture is drawn, don't update scroll. They will
+ // be updated when we get out of that mode.
+ // update our scroll so we don't appear to jump
+ // i.e. keep the center of the doc in the center of the view
+ int oldX = mWebView.getScrollX();
+ int oldY = mWebView.getScrollY();
+ float ratio = scale * oldInvScale;
+ float sx = ratio * oldX + (ratio - 1) * mZoomCenterX;
+ float sy = ratio * oldY + (ratio - 1)
+ * (mZoomCenterY - mWebView.getTitleHeight());
+
+ // Scale all the child views
+ mWebView.mViewManager.scaleAll();
+
+ // as we don't have animation for scaling, don't do animation
+ // for scrolling, as it causes weird intermediate state
+ int scrollX = mWebView.pinLocX(Math.round(sx));
+ int scrollY = mWebView.pinLocY(Math.round(sy));
+ if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) {
+ // the scroll position is adjusted at the beginning of the
+ // zoom animation. But we want to update the WebKit at the
+ // end of the zoom animation. See comments in onScaleEnd().
+ mWebView.sendOurVisibleRect();
+ }
+ }
+
+ // if the we need to reflow the text then force the VIEW_SIZE_CHANGED
+ // event to be sent to WebKit
+ mWebView.sendViewSizeZoom(reflowText);
+ }
+ }
+
+ /**
+ * The double tap gesture can result in different behaviors depending on the
+ * content that is tapped.
+ *
+ * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on
+ * the screen. If the plugin is already maximized then zoom the user into
+ * overview mode.
+ *
+ * (2) HTML/OTHER: If the taps occur outside a plugin then the following
+ * heuristic is used.
+ * A. If the current scale is not the same as the text wrap scale and the
+ * layout algorithm specifies the use of NARROW_COLUMNS, then fit to
+ * column by reflowing the text.
+ * B. If the page is not in overview mode then change to overview mode.
+ * C. If the page is in overmode then change to the default scale.
+ */
+ public void handleDoubleTap(float lastTouchX, float lastTouchY) {
+ WebSettings settings = mWebView.getSettings();
+ if (settings == null || settings.getUseWideViewPort() == false) {
+ return;
+ }
+
+ setZoomCenter(lastTouchX, lastTouchY);
+ mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX());
+ mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY());
+ settings.setDoubleTapToastCount(0);
+
+ // remove the zoom control after double tap
+ dismissZoomPicker();
+
+ /*
+ * If the double tap was on a plugin then either zoom to maximize the
+ * plugin on the screen or scale to overview mode.
+ */
+ ViewManager.ChildView plugin = mWebView.mViewManager.hitTest(mAnchorX, mAnchorY);
+ if (plugin != null) {
+ if (mWebView.isPluginFitOnScreen(plugin)) {
+ zoomToOverview();
+ } else {
+ mWebView.centerFitRect(plugin.x, plugin.y, plugin.width, plugin.height);
+ }
+ return;
+ }
+
+ if (settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS
+ && willScaleTriggerZoom(mTextWrapScale)) {
+ refreshZoomScale(true);
+ } else if (!mInZoomOverview) {
+ zoomToOverview();
+ } else {
+ zoomToDefaultLevel();
+ }
+ }
+
+ private void setZoomOverviewWidth(int width) {
+ mZoomOverviewWidth = width;
+ mInvZoomOverviewWidth = 1.0f / width;
+ }
+
+ private float getZoomOverviewScale() {
+ return mWebView.getViewWidth() * mInvZoomOverviewWidth;
+ }
+
+ public boolean isInZoomOverview() {
+ return mInZoomOverview;
+ }
+
+ private void zoomToOverview() {
+ if (!willScaleTriggerZoom(getZoomOverviewScale())) return;
+
+ // Force the titlebar fully reveal in overview mode
+ int scrollY = mWebView.getScrollY();
+ if (scrollY < mWebView.getTitleHeight()) {
+ mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0);
+ }
+ startZoomAnimation(getZoomOverviewScale(), true);
+ }
+
+ private void zoomToDefaultLevel() {
+ int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale);
+ if (left != WebView.NO_LEFTEDGE) {
+ // add a 5pt padding to the left edge.
+ int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5))
+ - mWebView.getScrollX();
+ // Re-calculate the zoom center so that the new scroll x will be
+ // on the left edge.
+ if (viewLeft > 0) {
+ mZoomCenterX = viewLeft * mDefaultScale / (mDefaultScale - mActualScale);
+ } else {
+ mWebView.scrollBy(viewLeft, 0);
+ mZoomCenterX = 0;
+ }
+ }
+ startZoomAnimation(mDefaultScale, true);
+ }
+
+ public void updateMultiTouchSupport(Context context) {
+ // check the preconditions
+ assert mWebView.getSettings() != null;
+
+ WebSettings settings = mWebView.getSettings();
+ mSupportMultiTouch = context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
+ && settings.supportZoom() && settings.getBuiltInZoomControls();
+ if (mSupportMultiTouch && (mScaleDetector == null)) {
+ mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener());
+ } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
+ mScaleDetector = null;
+ }
+ }
+
+ public boolean supportsMultiTouchZoom() {
+ return mSupportMultiTouch;
+ }
+
+ /**
+ * Notifies the caller that the ZoomManager is requesting that scale related
+ * updates should not be sent to webkit. This can occur in cases where the
+ * ZoomManager is performing an animation and does not want webkit to update
+ * until the animation is complete.
+ *
+ * @return true if scale related updates should not be sent to webkit and
+ * false otherwise.
+ */
+ public boolean isPreventingWebkitUpdates() {
+ // currently only animating a multi-touch zoom prevents updates, but
+ // others can add their own conditions to this method if necessary.
+ return mPinchToZoomAnimating;
+ }
+
+ public ScaleGestureDetector getMultiTouchGestureDetector() {
+ return mScaleDetector;
+ }
+
+ private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener {
+
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ dismissZoomPicker();
+ mWebView.mViewManager.startZoom();
+ mWebView.onPinchToZoomAnimationStart();
+ return true;
+ }
+
+ public boolean onScale(ScaleGestureDetector detector) {
+ float scale = Math.round(detector.getScaleFactor() * mActualScale * 100) * 0.01f;
+ if (willScaleTriggerZoom(scale)) {
+ mPinchToZoomAnimating = true;
+ // limit the scale change per step
+ if (scale > mActualScale) {
+ scale = Math.min(scale, mActualScale * 1.25f);
+ } else {
+ scale = Math.max(scale, mActualScale * 0.8f);
+ }
+ setZoomCenter(detector.getFocusX(), detector.getFocusY());
+ setZoomScale(scale, false);
+ mWebView.invalidate();
+ return true;
+ }
+ return false;
+ }
+
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ if (mPinchToZoomAnimating) {
+ mPinchToZoomAnimating = false;
+ mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
+ mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
+ // don't reflow when zoom in; when zoom out, do reflow if the
+ // new scale is almost minimum scale;
+ boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale);
+ // force zoom after mPreviewZoomOnly is set to false so that the
+ // new view size will be passed to the WebKit
+ refreshZoomScale(reflowNow);
+ // call invalidate() to draw without zoom filter
+ mWebView.invalidate();
+ }
+
+ mWebView.mViewManager.endZoom();
+ mWebView.onPinchToZoomAnimationEnd(detector);
+ }
+ }
+
+ public void onSizeChanged(int w, int h, int ow, int oh) {
+ // reset zoom and anchor to the top left corner of the screen
+ // unless we are already zooming
+ if (!isFixedLengthAnimationInProgress()) {
+ int visibleTitleHeight = mWebView.getVisibleTitleHeight();
+ mZoomCenterX = 0;
+ mZoomCenterY = visibleTitleHeight;
+ mAnchorX = mWebView.viewToContentX(mWebView.getScrollX());
+ mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY());
+ }
+
+ // update mMinZoomScale if the minimum zoom scale is not fixed
+ if (!mMinZoomScaleFixed) {
+ // when change from narrow screen to wide screen, the new viewWidth
+ // can be wider than the old content width. We limit the minimum
+ // scale to 1.0f. The proper minimum scale will be calculated when
+ // the new picture shows up.
+ mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth()
+ / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth()
+ : mZoomOverviewWidth));
+ // limit the minZoomScale to the initialScale if it is set
+ if (mInitialScale > 0 && mInitialScale < mMinZoomScale) {
+ mMinZoomScale = mInitialScale;
+ }
+ }
+
+ dismissZoomPicker();
+
+ // onSizeChanged() is called during WebView layout. And any
+ // requestLayout() is blocked during layout. As refreshZoomScale() will
+ // cause its child View to reposition itself through ViewManager's
+ // scaleAll(), we need to post a Runnable to ensure requestLayout().
+ // Additionally, only update the text wrap scale if the width changed.
+ mWebView.post(new PostScale(w != ow));
+ }
+
+ private class PostScale implements Runnable {
+ final boolean mUpdateTextWrap;
+
+ public PostScale(boolean updateTextWrap) {
+ mUpdateTextWrap = updateTextWrap;
+ }
+
+ public void run() {
+ if (mWebView.getWebViewCore() != null) {
+ // we always force, in case our height changed, in which case we
+ // still want to send the notification over to webkit.
+ refreshZoomScale(mUpdateTextWrap);
+ // update the zoom buttons as the scale can be changed
+ updateZoomPicker();
+ }
+ }
+ }
+
+ public void updateZoomRange(WebViewCore.ViewState viewState,
+ int viewWidth, int minPrefWidth) {
+ if (viewState.mMinScale == 0) {
+ if (viewState.mMobileSite) {
+ if (minPrefWidth > Math.max(0, viewWidth)) {
+ mMinZoomScale = (float) viewWidth / minPrefWidth;
+ mMinZoomScaleFixed = false;
+ } else {
+ mMinZoomScale = viewState.mDefaultScale;
+ mMinZoomScaleFixed = true;
+ }
+ } else {
+ mMinZoomScale = mDefaultMinZoomScale;
+ mMinZoomScaleFixed = false;
+ }
+ } else {
+ mMinZoomScale = viewState.mMinScale;
+ mMinZoomScaleFixed = true;
+ }
+ if (viewState.mMaxScale == 0) {
+ mMaxZoomScale = mDefaultMaxZoomScale;
+ } else {
+ mMaxZoomScale = viewState.mMaxScale;
+ }
+ }
+
+ /**
+ * Updates zoom values when Webkit produces a new picture. This method
+ * should only be called from the UI thread's message handler.
+ */
+ public void onNewPicture(WebViewCore.DrawData drawData) {
+ final int viewWidth = mWebView.getViewWidth();
+
+ if (mWebView.getSettings().getUseWideViewPort()) {
+ // limit mZoomOverviewWidth upper bound to
+ // sMaxViewportWidth so that if the page doesn't behave
+ // well, the WebView won't go insane. limit the lower
+ // bound to match the default scale for mobile sites.
+ setZoomOverviewWidth(Math.min(WebView.sMaxViewportWidth,
+ Math.max((int) (viewWidth * mInvDefaultScale),
+ Math.max(drawData.mMinPrefWidth, drawData.mViewPoint.x))));
+ }
+
+ final float zoomOverviewScale = getZoomOverviewScale();
+ if (!mMinZoomScaleFixed) {
+ mMinZoomScale = zoomOverviewScale;
+ }
+ // fit the content width to the current view. Ignore the rounding error case.
+ if (!mWebView.drawHistory() && (mInitialZoomOverview || (mInZoomOverview
+ && Math.abs((viewWidth * mInvActualScale) - mZoomOverviewWidth) > 1))) {
+ mInitialZoomOverview = false;
+ setZoomScale(zoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale));
+ }
+ }
+
+ /**
+ * Updates zoom values after Webkit completes the initial page layout. It
+ * is called when visiting a page for the first time as well as when the
+ * user navigates back to a page (in which case we may need to restore the
+ * zoom levels to the state they were when you left the page). This method
+ * should only be called from the UI thread's message handler.
+ */
+ public void onFirstLayout(WebViewCore.DrawData drawData) {
+ // precondition check
+ assert drawData != null;
+ assert drawData.mViewState != null;
+ assert mWebView.getSettings() != null;
+
+ WebViewCore.ViewState viewState = drawData.mViewState;
+ final Point viewSize = drawData.mViewPoint;
+ updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth);
+
+ if (!mWebView.drawHistory()) {
+ final float scale;
+ final boolean reflowText;
+
+ if (mInitialScale > 0) {
+ scale = mInitialScale;
+ reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale);
+ } else if (viewState.mViewScale > 0) {
+ mTextWrapScale = viewState.mTextWrapScale;
+ scale = viewState.mViewScale;
+ reflowText = false;
+ } else {
+ WebSettings settings = mWebView.getSettings();
+ if (settings.getUseWideViewPort() && settings.getLoadWithOverviewMode()) {
+ mInitialZoomOverview = true;
+ scale = (float) mWebView.getViewWidth() / WebView.DEFAULT_VIEWPORT_WIDTH;
+ } else {
+ scale = viewState.mTextWrapScale;
+ }
+ reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale);
+ }
+ setZoomScale(scale, reflowText);
+
+ // update the zoom buttons as the scale can be changed
+ updateZoomPicker();
+ }
+ }
+
+ public void saveZoomState(Bundle b) {
+ b.putFloat("scale", mActualScale);
+ b.putFloat("textwrapScale", mTextWrapScale);
+ b.putBoolean("overview", mInZoomOverview);
+ }
+
+ public void restoreZoomState(Bundle b) {
+ // as getWidth() / getHeight() of the view are not available yet, set up
+ // mActualScale, so that when onSizeChanged() is called, the rest will
+ // be set correctly
+ mActualScale = b.getFloat("scale", 1.0f);
+ mInvActualScale = 1 / mActualScale;
+ mTextWrapScale = b.getFloat("textwrapScale", mActualScale);
+ mInZoomOverview = b.getBoolean("overview");
+ }
+
+ private ZoomControlBase getCurrentZoomControl() {
+ if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) {
+ if (mWebView.getSettings().getBuiltInZoomControls()) {
+ if (mEmbeddedZoomControl == null) {
+ mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView);
+ }
+ return mEmbeddedZoomControl;
+ } else {
+ if (mExternalZoomControl == null) {
+ mExternalZoomControl = new ZoomControlExternal(mWebView);
+ }
+ return mExternalZoomControl;
+ }
+ }
+ return null;
+ }
+
+ public void invokeZoomPicker() {
+ ZoomControlBase control = getCurrentZoomControl();
+ if (control != null) {
+ control.show();
+ }
+ }
+
+ public void dismissZoomPicker() {
+ ZoomControlBase control = getCurrentZoomControl();
+ if (control != null) {
+ control.hide();
+ }
+ }
+
+ public boolean isZoomPickerVisible() {
+ ZoomControlBase control = getCurrentZoomControl();
+ return (control != null) ? control.isVisible() : false;
+ }
+
+ public void updateZoomPicker() {
+ ZoomControlBase control = getCurrentZoomControl();
+ if (control != null) {
+ control.update();
+ }
+ }
+
+ /**
+ * The embedded zoom control intercepts touch events and automatically stays
+ * visible. The external control needs to constantly refresh its internal
+ * timer to stay visible.
+ */
+ public void keepZoomPickerVisible() {
+ ZoomControlBase control = getCurrentZoomControl();
+ if (control != null && control == mExternalZoomControl) {
+ control.show();
+ }
+ }
+
+ public View getExternalZoomPicker() {
+ ZoomControlBase control = getCurrentZoomControl();
+ if (control != null && control == mExternalZoomControl) {
+ return mExternalZoomControl.getControls();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/java/android/webruntime/WebRuntimeActivity.java b/core/java/android/webruntime/WebRuntimeActivity.java
new file mode 100644
index 0000000..ec8c60c
--- /dev/null
+++ b/core/java/android/webruntime/WebRuntimeActivity.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webruntime;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+import android.view.Window;
+import android.webkit.GeolocationPermissions;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.android.internal.R;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * The runtime used to display installed web applications.
+ * @hide
+ */
+public class WebRuntimeActivity extends Activity
+{
+ private final static String LOGTAG = "WebRuntimeActivity";
+
+ private WebView mWebView;
+ private URL mBaseUrl;
+ private ImageView mSplashScreen;
+
+ public static class SensitiveFeatures {
+ // All of the sensitive features
+ private boolean mGeolocation;
+ // On Android, the Browser doesn't prompt for database access, so we don't require an
+ // explicit permission here in the WebRuntimeActivity, and there's no Android system
+ // permission required for it either.
+ //private boolean mDatabase;
+
+ public boolean getGeolocation() {
+ return mGeolocation;
+ }
+ public void setGeolocation(boolean geolocation) {
+ mGeolocation = geolocation;
+ }
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ // Can't get meta data using getApplicationInfo() as it doesn't pass GET_META_DATA
+ PackageManager packageManager = getPackageManager();
+ ComponentName componentName = new ComponentName(this, getClass());
+ ActivityInfo activityInfo = null;
+ try {
+ activityInfo = packageManager.getActivityInfo(componentName, PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(LOGTAG, "Failed to find component");
+ return;
+ }
+ if (activityInfo == null) {
+ Log.d(LOGTAG, "Failed to get activity info");
+ return;
+ }
+
+ Bundle metaData = activityInfo.metaData;
+ if (metaData == null) {
+ Log.d(LOGTAG, "No meta data");
+ return;
+ }
+
+ String url = metaData.getString("android.webruntime.url");
+ if (url == null) {
+ Log.d(LOGTAG, "No URL");
+ return;
+ }
+
+ try {
+ mBaseUrl = new URL(url);
+ } catch (MalformedURLException e) {
+ Log.d(LOGTAG, "Invalid URL");
+ }
+
+ // All false by default, and reading non-existent bundle properties gives false too.
+ final SensitiveFeatures sensitiveFeatures = new SensitiveFeatures();
+ sensitiveFeatures.setGeolocation(metaData.getBoolean("android.webruntime.SensitiveFeaturesGeolocation"));
+
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.web_runtime);
+ mWebView = (WebView) findViewById(R.id.webview);
+ mSplashScreen = (ImageView) findViewById(R.id.splashscreen);
+ mSplashScreen.setImageResource(
+ getResources().getIdentifier("splash_screen", "drawable", getPackageName()));
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ try {
+ URL newOrigin = new URL(url);
+ if (areSameOrigin(mBaseUrl, newOrigin)) {
+ // If simple same origin test passes, load in the webview.
+ return false;
+ }
+ } catch(MalformedURLException e) {
+ // Don't load anything if this wasn't a proper URL.
+ return true;
+ }
+
+ // Otherwise this is a URL that is not same origin so pass it to the
+ // Browser to load.
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
+ return true;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ if (mSplashScreen != null && mSplashScreen.getVisibility() == View.VISIBLE) {
+ mSplashScreen.setVisibility(View.GONE);
+ mSplashScreen = null;
+ }
+ }
+ });
+
+ // Use a custom WebChromeClient with geolocation permissions handling.
+ mWebView.setWebChromeClient(new WebChromeClient() {
+ public void onGeolocationPermissionsShowPrompt(
+ String origin, GeolocationPermissions.Callback callback) {
+ // Allow this origin if it has Geolocation permissions, otherwise deny.
+ boolean allowed = false;
+ if (sensitiveFeatures.getGeolocation()) {
+ try {
+ URL originUrl = new URL(origin);
+ allowed = areSameOrigin(mBaseUrl, originUrl);
+ } catch(MalformedURLException e) {
+ }
+ }
+ callback.invoke(origin, allowed, false);
+ }
+ });
+
+ // Set the DB location. Optional. Geolocation works without DBs.
+ mWebView.getSettings().setGeolocationDatabasePath(
+ getDir("geolocation", MODE_PRIVATE).getPath());
+
+ String title = metaData.getString("android.webruntime.title");
+ // We turned off the title bar to go full screen so display the
+ // webapp's title as a toast.
+ if (title != null) {
+ Toast.makeText(this, title, Toast.LENGTH_SHORT).show();
+ }
+
+ // Load the webapp's base URL.
+ mWebView.loadUrl(url);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
+ mWebView.goBack();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, 0, 0, "Menu item 1");
+ menu.add(0, 1, 0, "Menu item 2");
+ return true;
+ }
+
+ private static boolean areSameOrigin(URL a, URL b) {
+ int aPort = a.getPort() == -1 ? a.getDefaultPort() : a.getPort();
+ int bPort = b.getPort() == -1 ? b.getDefaultPort() : b.getPort();
+ return a.getProtocol().equals(b.getProtocol()) && aPort == bPort && a.getHost().equals(b.getHost());
+ }
+}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 6cfeb68..70c1e15 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -559,6 +559,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true);
setSmoothScrollbarEnabled(smoothScrollbar);
+
+ final int adapterId = a.getResourceId(R.styleable.AbsListView_adapter, 0);
+ if (adapterId != 0) {
+ final Context c = context;
+ post(new Runnable() {
+ public void run() {
+ setAdapter(Adapters.loadAdapter(c, adapterId));
+ }
+ });
+ }
a.recycle();
}
@@ -1575,6 +1585,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
treeObserver.addOnGlobalLayoutListener(this);
}
}
+
+ if (mAdapter != null && mDataSetObserver == null) {
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+ }
}
@Override
@@ -1595,6 +1610,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
mGlobalLayoutListenerAddedFilter = false;
}
}
+
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ mDataSetObserver = null;
+ }
}
@Override
@@ -2521,6 +2541,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
private static final int MOVE_UP_POS = 2;
private static final int MOVE_DOWN_BOUND = 3;
private static final int MOVE_UP_BOUND = 4;
+ private static final int MOVE_OFFSET = 5;
private int mMode;
private int mTargetPos;
@@ -2528,6 +2549,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
private int mLastSeenPos;
private int mScrollDuration;
private final int mExtraScroll;
+
+ private int mOffsetFromTop;
PositionScroller() {
mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
@@ -2619,12 +2642,46 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
post(this);
}
-
+
+ void startWithOffset(int position, int offset) {
+ mTargetPos = position;
+ mOffsetFromTop = offset;
+ mBoundPos = INVALID_POSITION;
+ mLastSeenPos = INVALID_POSITION;
+ mMode = MOVE_OFFSET;
+
+ final int firstPos = mFirstPosition;
+ final int childCount = getChildCount();
+ final int lastPos = firstPos + childCount - 1;
+
+ int viewTravelCount = 0;
+ if (position < firstPos) {
+ viewTravelCount = firstPos - position;
+ } else if (position > lastPos) {
+ viewTravelCount = position - lastPos;
+ } else {
+ // On-screen, just scroll.
+ final int targetTop = getChildAt(position - firstPos).getTop();
+ smoothScrollBy(targetTop - offset, SCROLL_DURATION);
+ return;
+ }
+
+ // Estimate how many screens we should travel
+ final float screenTravelCount = viewTravelCount / childCount;
+ mScrollDuration = (int) (SCROLL_DURATION / screenTravelCount);
+ mLastSeenPos = INVALID_POSITION;
+ post(this);
+ }
+
void stop() {
removeCallbacks(this);
}
-
+
public void run() {
+ if (mTouchMode != TOUCH_MODE_FLING && mLastSeenPos != INVALID_POSITION) {
+ return;
+ }
+
final int listHeight = getHeight();
final int firstPos = mFirstPosition;
@@ -2749,6 +2806,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
break;
}
+ case MOVE_OFFSET: {
+ final int childCount = getChildCount();
+
+ mLastSeenPos = firstPos;
+ final int position = mTargetPos;
+ final int lastPos = firstPos + childCount - 1;
+
+ if (position < firstPos) {
+ smoothScrollBy(-getHeight(), mScrollDuration);
+ post(this);
+ } else if (position > lastPos) {
+ smoothScrollBy(getHeight(), mScrollDuration);
+ post(this);
+ } else {
+ // On-screen, just scroll.
+ final int targetTop = getChildAt(position - firstPos).getTop();
+ smoothScrollBy(targetTop - mOffsetFromTop, mScrollDuration);
+ }
+ break;
+ }
+
default:
break;
}
@@ -2768,6 +2846,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
/**
+ * Smoothly scroll to the specified adapter position. The view will scroll
+ * such that the indicated position is displayed <code>offset</code> pixels from
+ * the top edge of the view. If this is impossible, (e.g. the offset would scroll
+ * the first or last item beyond the boundaries of the list) it will get as close
+ * as possible.
+ *
+ * @param position Position to scroll to
+ * @param offset Desired distance in pixels of <code>position</code> from the top
+ * of the view when scrolling is finished
+ */
+ public void smoothScrollToPositionFromTop(int position, int offset) {
+ if (mPositionScroller == null) {
+ mPositionScroller = new PositionScroller();
+ }
+ mPositionScroller.startWithOffset(position, offset);
+ }
+
+ /**
* Smoothly scroll to the specified adapter position. The view will
* scroll such that the indicated position is displayed, but it will
* stop early if scrolling further would scroll boundPosition out of
@@ -3155,6 +3251,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
mResurrectToPosition = INVALID_POSITION;
removeCallbacks(mFlingRunnable);
+ removeCallbacks(mPositionScroller);
mTouchMode = TOUCH_MODE_REST;
clearScrollingCache();
mSpecificTop = selectedTop;
@@ -4190,7 +4287,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
- scrap.get(i).setDrawingCacheBackgroundColor(color);
+ scrap.get(j).setDrawingCacheBackgroundColor(color);
}
}
}
diff --git a/core/java/android/widget/Adapters.java b/core/java/android/widget/Adapters.java
new file mode 100644
index 0000000..7fd7fb5
--- /dev/null
+++ b/core/java/android/widget/Adapters.java
@@ -0,0 +1,1232 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.View;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * <p>This class can be used to load {@link android.widget.Adapter adapters} defined in
+ * XML resources. XML-defined adapters can be used to easily create adapters in your
+ * own application or to pass adapters to other processes.</p>
+ *
+ * <h2>Types of adapters</h2>
+ * <p>Adapters defined using XML resources can only be one of the following supported
+ * types. Arbitrary adapters are not supported to guarantee the safety of the loaded
+ * code when adapters are loaded across packages.</p>
+ * <ul>
+ * <li><a href="#xml-cursor-adapter">Cursor adapter</a>: a cursor adapter can be used
+ * to display the content of a cursor, most often coming from a content provider</li>
+ * </ul>
+ * <p>The complete XML format definition of each adapter type is available below.</p>
+ *
+ * <a name="xml-cursor-adapter" />
+ * <h2>Cursor adapter</h2>
+ * <p>A cursor adapter XML definition starts with the
+ * <a href="#xml-cursor-adapter-tag"><code>&lt;cursor-adapter /&gt;</code></a>
+ * tag and may contain one or more instances of the following tags:</p>
+ * <ul>
+ * <li><a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code></a></li>
+ * <li><a href="#xml-cursor-adapter-bind-tag"><code>&lt;bind /&gt;</code></a></li>
+ * </ul>
+ *
+ * <a name="xml-cursor-adapter-tag" />
+ * <h3>&lt;cursor-adapter /&gt;</h3>
+ * <p>The <code>&lt;cursor-adapter /&gt;</code> element defines the beginning of the
+ * document and supports the following attributes:</p>
+ * <ul>
+ * <li><code>android:layout</code>: Reference to the XML layout to be inflated for
+ * each item of the adapter. This attribute is mandatory.</li>
+ * <li><code>android:selection</code>: Selection expression, used when the
+ * <code>android:uri</code> attribute is defined or when the adapter is loaded with
+ * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
+ * This attribute is optional.</li>
+ * <li><code>android:sortOrder</code>: Sort expression, used when the
+ * <code>android:uri</code> attribute is defined or when the adapter is loaded with
+ * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
+ * This attribute is optional.</li>
+ * <li><code>android:uri</code>: URI of the content provider to query to retrieve a cursor.
+ * Specifying this attribute is equivalent to calling
+ * {@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
+ * If you call this method, the value of the XML attribute is ignored. This attribute is
+ * optional.</li>
+ * </ul>
+ * <p>In addition, you can specify one or more instances of
+ * <a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code></a> and
+ * <a href="#xml-cursor-adapter-bind-tag"><code>&lt;bind /&gt;</code></a> tags as children
+ * of <code>&lt;cursor-adapter /&gt;</code>.</p>
+ *
+ * <a name="xml-cursor-adapter-select-tag" />
+ * <h3>&lt;select /&gt;</h3>
+ * <p>The <code>&lt;select /&gt;</code> tag is used to select columns from the cursor
+ * when doing the query. This can be very useful when using transformations in the
+ * <code>&lt;bind /&gt;</code> elements. It can also be very useful if you are providing
+ * your own <a href="#xml-cursor-adapter-bind-data-types">binder</a> or
+ * <a href="#xml-cursor-adapter-bind-data-types">transformation</a> classes.
+ * <code>&lt;select /&gt;</code> elements are ignored if you supply the cursor yourself.</p>
+ * <p>The <code>&lt;select /&gt;</code> supports the following attributes:</p>
+ * <ul>
+ * <li><code>android:column</code>: Name of the column to select in the cursor during the
+ * query operation</li>
+ * </ul>
+ * <p><strong>Note:</strong> The column named <code>_id</code> is always implicitly
+ * selected.</p>
+ *
+ * <a name="xml-cursor-adapter-bind-tag" />
+ * <h3>&lt;bind /&gt;</h3>
+ * <p>The <code>&lt;bind /&gt;</code> tag is used to bind a column from the cursor to
+ * a {@link android.view.View}. A column bound using this tag is automatically selected
+ * during the query and a matching
+ * <a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code> tag is therefore
+ * not required.</p>
+ *
+ * <p>Each binding is declared as a one to one matching but
+ * custom binder classes or special
+ * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> can
+ * allow you to bind several columns to a single view. In this case you must use the
+ * <a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code> tag to make
+ * sure any required column is part of the query.</p>
+ *
+ * <p>The <code>&lt;bind /&gt;</code> tag supports the following attributes:</p>
+ * <ul>
+ * <li><code>android:from</code>: The name of the column to bind from.
+ * This attribute is mandatory. Note that <code>@</code> which are not used to reference resources
+ * should be backslash protected as in <code>\@</code>.</li>
+ * <li><code>android:to</code>: The id of the view to bind to. This attribute is mandatory.</li>
+ * <li><code>android:as</code>: The <a href="#xml-cursor-adapter-bind-data-types">data type</a>
+ * of the binding. This attribute is mandatory.</li>
+ * </ul>
+ *
+ * <p>In addition, a <code>&lt;bind /&gt;</code> can contain zero or more instances of
+ * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> children
+ * tags.</p>
+ *
+ * <a name="xml-cursor-adapter-bind-data-types" />
+ * <h4>Binding data types</h4>
+ * <p>For a binding to occur the data type of the bound column/view pair must be specified.
+ * The following data types are currently supported:</p>
+ * <ul>
+ * <li><code>string</code>: The content of the column is interpreted as a string and must be
+ * bound to a {@link android.widget.TextView}</li>
+ * <li><code>image</code>: The content of the column is interpreted as a blob describing an
+ * image and must be bound to an {@link android.widget.ImageView}</li>
+ * <li><code>image-uri</code>: The content of the column is interpreted as a URI to an image
+ * and must be bound to an {@link android.widget.ImageView}</li>
+ * <li><code>drawable</code>: The content of the column is interpreted as a resource id to a
+ * drawable and must be bound to an {@link android.widget.ImageView}</li>
+ * <li><code>tag</code>: The content of the column is interpreted as a string and will be set as
+ * the tag (using {@link View#setTag(Object)} of the associated View. This can be used to
+ * associate meta-data to your view, that can be used for instance by a listener.</li>
+ * <li>A fully qualified class name: The name of a class corresponding to an implementation of
+ * {@link android.widget.Adapters.CursorBinder}. Cursor binders can be used to provide
+ * bindings not supported by default. Custom binders cannot be used with
+ * {@link android.content.Context#isRestricted() restricted contexts}, for instance in an
+ * application widget</li>
+ * </ul>
+ *
+ * <a name="xml-cursor-adapter-bind-transformation" />
+ * <h4>Binding transformations</h4>
+ * <p>When defining a data binding you can specify an optional transformation by using one
+ * of the following tags as a child of a <code>&lt;bind /&gt;</code> elements:</p>
+ * <ul>
+ * <li><code>&lt;map /&gt;</code>: Maps a constant string to a string or a resource. Use
+ * one instance of this tag per value you want to map</li>
+ * <li><code>&lt;transform /&gt;</code>: Transforms a column's value using an expression
+ * or an instance of {@link android.widget.Adapters.CursorTransformation}</li>
+ * </ul>
+ * <p>While several <code>&lt;map /&gt;</code> tags can be used at the same time, you cannot
+ * mix <code>&lt;map /&gt;</code> and <code>&lt;transform /&gt;</code> tags. If several
+ * <code>&lt;transform /&gt;</code> tags are specified, only the last one is retained.</p>
+ *
+ * <a name="xml-cursor-adapter-bind-transformation-map" />
+ * <p><strong>&lt;map /&gt;</strong></p>
+ * <p>A map element simply specifies a value to match from and a value to match to. When
+ * a column's value equals the value to match from, it is replaced with the value to match
+ * to. The following attributes are supported:</p>
+ * <ul>
+ * <li><code>android:fromValue</code>: The value to match from. This attribute is mandatory</li>
+ * <li><code>android:toValue</code>: The value to match to. This value can be either a string
+ * or a resource identifier. This value is interpreted as a resource identifier when the
+ * data binding is of type <code>drawable</code>. This attribute is mandatory</li>
+ * </ul>
+ *
+ * <a name="xml-cursor-adapter-bind-transformation-transform" />
+ * <p><strong>&lt;transform /&gt;</strong></p>
+ * <p>A simple transform that occurs either by calling a specified class or by performing
+ * simple text substitution. The following attributes are supported:</p>
+ * <ul>
+ * <li><code>android:withExpression</code>: The transformation expression. The expression is
+ * a string containing column names surrounded with curly braces { and }. During the
+ * transformation each column name is replaced by its value. All columns must have been
+ * selected in the query. An example of expression is <code>"First name: {first_name},
+ * last name: {last_name}"</code>. This attribute is mandatory
+ * if <code>android:withClass</code> is not specified and ignored if <code>android:withClass</code>
+ * is specified</li>
+ * <li><code>android:withClass</code>: A fully qualified class name corresponding to an
+ * implementation of {@link android.widget.Adapters.CursorTransformation}. Custom
+ * transformations cannot be used with
+ * {@link android.content.Context#isRestricted() restricted contexts}, for instance in
+ * an app widget This attribute is mandatory if <code>android:withExpression</code> is
+ * not specified</li>
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * <p>The following example defines a cursor adapter that queries all the contacts with
+ * a phone number using the contacts content provider. Each contact is displayed with
+ * its display name, its favorite status and its photo. To display photos, a custom data
+ * binder is declared:</p>
+ *
+ * <pre class="prettyprint">
+ * &lt;cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:uri="content://com.android.contacts/contacts"
+ * android:selection="has_phone_number=1"
+ * android:layout="@layout/contact_item"&gt;
+ *
+ * &lt;bind android:from="display_name" android:to="@id/name" android:as="string" /&gt;
+ * &lt;bind android:from="starred" android:to="@id/star" android:as="drawable"&gt;
+ * &lt;map android:fromValue="0" android:toValue="@android:drawable/star_big_off" /&gt;
+ * &lt;map android:fromValue="1" android:toValue="@android:drawable/star_big_on" /&gt;
+ * &lt;/bind&gt;
+ * &lt;bind android:from="_id" android:to="@id/name"
+ * android:as="com.google.android.test.adapters.ContactPhotoBinder" /&gt;
+ *
+ * &lt;/cursor-adapter&gt;
+ * </pre>
+ *
+ * <h3>Related APIs</h3>
+ * <ul>
+ * <li>{@link android.widget.Adapters#loadAdapter(android.content.Context, int, Object[])}</li>
+ * <li>{@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])}</li>
+ * <li>{@link android.widget.Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}</li>
+ * <li>{@link android.widget.Adapters.CursorBinder}</li>
+ * <li>{@link android.widget.Adapters.CursorTransformation}</li>
+ * <li>{@link android.widget.CursorAdapter}</li>
+ * </ul>
+ *
+ * @see android.widget.Adapter
+ * @see android.content.ContentProvider
+ *
+ * @attr ref android.R.styleable#CursorAdapter_layout
+ * @attr ref android.R.styleable#CursorAdapter_selection
+ * @attr ref android.R.styleable#CursorAdapter_sortOrder
+ * @attr ref android.R.styleable#CursorAdapter_uri
+ * @attr ref android.R.styleable#CursorAdapter_BindItem_as
+ * @attr ref android.R.styleable#CursorAdapter_BindItem_from
+ * @attr ref android.R.styleable#CursorAdapter_BindItem_to
+ * @attr ref android.R.styleable#CursorAdapter_MapItem_fromValue
+ * @attr ref android.R.styleable#CursorAdapter_MapItem_toValue
+ * @attr ref android.R.styleable#CursorAdapter_SelectItem_column
+ * @attr ref android.R.styleable#CursorAdapter_TransformItem_withClass
+ * @attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression
+ */
+@SuppressWarnings({"JavadocReference"})
+public class Adapters {
+ private static final String ADAPTER_CURSOR = "cursor-adapter";
+
+ /**
+ * <p>Interface used to bind a {@link android.database.Cursor} column to a View. This
+ * interface can be used to provide bindings for data types not supported by the
+ * standard implementation of {@link android.widget.Adapters}.</p>
+ *
+ * <p>A binder is provided with a cursor transformation which may or may not be used
+ * to transform the value retrieved from the cursor. The transformation is guaranteed
+ * to never be null so it's always safe to apply the transformation.</p>
+ *
+ * <p>The binder is associated with a Context but can be re-used with multiple cursors.
+ * As such, the implementation should make no assumption about the Cursor in use.</p>
+ *
+ * @see android.view.View
+ * @see android.database.Cursor
+ * @see android.widget.Adapters.CursorTransformation
+ */
+ public static abstract class CursorBinder {
+ /**
+ * <p>The context associated with this binder.</p>
+ */
+ protected final Context mContext;
+
+ /**
+ * <p>The transformation associated with this binder. This transformation is never
+ * null and may or may not be applied to the Cursor data during the
+ * {@link #bind(android.view.View, android.database.Cursor, int)} operation.</p>
+ *
+ * @see #bind(android.view.View, android.database.Cursor, int)
+ */
+ protected final CursorTransformation mTransformation;
+
+ /**
+ * <p>Creates a new Cursor binder.</p>
+ *
+ * @param context The context associated with this binder.
+ * @param transformation The transformation associated with this binder. This
+ * transformation may or may not be applied by the binder and is guaranteed
+ * to not be null.
+ */
+ public CursorBinder(Context context, CursorTransformation transformation) {
+ mContext = context;
+ mTransformation = transformation;
+ }
+
+ /**
+ * <p>Binds the specified Cursor column to the supplied View. The binding operation
+ * can query other Cursor columns as needed. During the binding operation, values
+ * retrieved from the Cursor may or may not be transformed using this binder's
+ * cursor transformation.</p>
+ *
+ * @param view The view to bind data to.
+ * @param cursor The cursor to bind data from.
+ * @param columnIndex The column index in the cursor where the data to bind resides.
+ *
+ * @see #mTransformation
+ *
+ * @return True if the column was successfully bound to the View, false otherwise.
+ */
+ public abstract boolean bind(View view, Cursor cursor, int columnIndex);
+ }
+
+ /**
+ * <p>Interface used to transform data coming out of a {@link android.database.Cursor}
+ * before it is bound to a {@link android.view.View}.</p>
+ *
+ * <p>Transformations are used to transform text-based data (in the form of a String),
+ * or to transform data into a resource identifier. A default implementation is provided
+ * to generate resource identifiers.</p>
+ *
+ * @see android.database.Cursor
+ * @see android.widget.Adapters.CursorBinder
+ */
+ public static abstract class CursorTransformation {
+ /**
+ * <p>The context associated with this transformation.</p>
+ */
+ protected final Context mContext;
+
+ /**
+ * <p>Creates a new Cursor transformation.</p>
+ *
+ * @param context The context associated with this transformation.
+ */
+ public CursorTransformation(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * <p>Transforms the specified Cursor column into a String. The transformation
+ * can simply return the content of the column as a String (this is known
+ * as the identity transformation) or manipulate the content. For instance,
+ * a transformation can perform text substitutions or concatenate other
+ * columns with the specified column.</p>
+ *
+ * @param cursor The cursor that contains the data to transform.
+ * @param columnIndex The index of the column to transform.
+ *
+ * @return A String containing the transformed value of the column.
+ */
+ public abstract String transform(Cursor cursor, int columnIndex);
+
+ /**
+ * <p>Transforms the specified Cursor column into a resource identifier.
+ * The default implementation simply interprets the content of the column
+ * as an integer.</p>
+ *
+ * @param cursor The cursor that contains the data to transform.
+ * @param columnIndex The index of the column to transform.
+ *
+ * @return A resource identifier.
+ */
+ public int transformToResource(Cursor cursor, int columnIndex) {
+ return cursor.getInt(columnIndex);
+ }
+ }
+
+ /**
+ * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified
+ * XML resource. The content of the adapter is loaded from the content provider
+ * identified by the supplied URI.</p>
+ *
+ * <p><strong>Note:</strong> If the supplied {@link android.content.Context} is
+ * an {@link android.app.Activity}, the cursor returned by the content provider
+ * will be automatically managed. Otherwise, you are responsible for managing the
+ * cursor yourself.</p>
+ *
+ * <p>The format of the XML definition of the cursor adapter is documented at
+ * the top of this page.</p>
+ *
+ * @param context The context to load the XML resource from.
+ * @param id The identifier of the XML resource declaring the adapter.
+ * @param uri The URI of the content provider.
+ * @param parameters Optional parameters to pass to the CursorAdapter, used
+ * to substitute values in the selection expression.
+ *
+ * @return A {@link android.widget.CursorAdapter}
+ *
+ * @throws IllegalArgumentException If the XML resource does not contain
+ * a valid &lt;cursor-adapter /&gt; definition.
+ *
+ * @see android.content.ContentProvider
+ * @see android.widget.CursorAdapter
+ * @see #loadAdapter(android.content.Context, int, Object[])
+ */
+ public static CursorAdapter loadCursorAdapter(Context context, int id, String uri,
+ Object... parameters) {
+
+ XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR,
+ parameters);
+
+ if (uri != null) {
+ adapter.setUri(uri);
+ }
+ adapter.load();
+
+ return adapter;
+ }
+
+ /**
+ * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified
+ * XML resource. The content of the adapter is loaded from the specified cursor.
+ * You are responsible for managing the supplied cursor.</p>
+ *
+ * <p>The format of the XML definition of the cursor adapter is documented at
+ * the top of this page.</p>
+ *
+ * @param context The context to load the XML resource from.
+ * @param id The identifier of the XML resource declaring the adapter.
+ * @param cursor The cursor containing the data for the adapter.
+ * @param parameters Optional parameters to pass to the CursorAdapter, used
+ * to substitute values in the selection expression.
+ *
+ * @return A {@link android.widget.CursorAdapter}
+ *
+ * @throws IllegalArgumentException If the XML resource does not contain
+ * a valid &lt;cursor-adapter /&gt; definition.
+ *
+ * @see android.content.ContentProvider
+ * @see android.widget.CursorAdapter
+ * @see android.database.Cursor
+ * @see #loadAdapter(android.content.Context, int, Object[])
+ */
+ public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor,
+ Object... parameters) {
+
+ XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR,
+ parameters);
+
+ if (cursor != null) {
+ adapter.changeCursor(cursor);
+ }
+
+ return adapter;
+ }
+
+ /**
+ * <p>Loads the adapter defined in the specified XML resource. The XML definition of
+ * the adapter must follow the format definition of one of the supported adapter
+ * types described at the top of this page.</p>
+ *
+ * <p><strong>Note:</strong> If the loaded adapter is a {@link android.widget.CursorAdapter}
+ * and the supplied {@link android.content.Context} is an {@link android.app.Activity},
+ * the cursor returned by the content provider will be automatically managed. Otherwise,
+ * you are responsible for managing the cursor yourself.</p>
+ *
+ * @param context The context to load the XML resource from.
+ * @param id The identifier of the XML resource declaring the adapter.
+ * @param parameters Optional parameters to pass to the adapter.
+ *
+ * @return An adapter instance.
+ *
+ * @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])
+ * @see #loadCursorAdapter(android.content.Context, int, String, Object[])
+ */
+ public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) {
+ final BaseAdapter adapter = loadAdapter(context, id, null, parameters);
+ if (adapter instanceof ManagedAdapter) {
+ ((ManagedAdapter) adapter).load();
+ }
+ return adapter;
+ }
+
+ /**
+ * Loads an adapter from the specified XML resource. The optional assertName can
+ * be used to exit early if the adapter defined in the XML resource is not of the
+ * expected type.
+ *
+ * @param context The context to associate with the adapter.
+ * @param id The resource id of the XML document defining the adapter.
+ * @param assertName The mandatory name of the adapter in the XML document.
+ * Ignored if null.
+ * @param parameters Optional parameters passed to the adapter.
+ *
+ * @return An instance of {@link android.widget.BaseAdapter}.
+ */
+ private static BaseAdapter loadAdapter(Context context, int id, String assertName,
+ Object... parameters) {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getXml(id);
+ return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser),
+ id, parameters, assertName);
+ } catch (XmlPullParserException ex) {
+ Resources.NotFoundException rnf = new Resources.NotFoundException(
+ "Can't load adapter resource ID " +
+ context.getResources().getResourceEntryName(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ Resources.NotFoundException rnf = new Resources.NotFoundException(
+ "Can't load adapter resource ID " +
+ context.getResources().getResourceEntryName(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ /**
+ * Generates an adapter using the specified XML parser. This method is responsible
+ * for choosing the type of the adapter to create based on the content of the
+ * XML parser.
+ *
+ * This method will generate an {@link IllegalArgumentException} if
+ * <code>assertName</code> is not null and does not match the root tag of the XML
+ * document.
+ */
+ private static BaseAdapter createAdapterFromXml(Context c,
+ XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters,
+ String assertName) throws XmlPullParserException, IOException {
+
+ BaseAdapter adapter = null;
+
+ // Make sure we are on a start tag.
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
+ type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+ if (assertName != null && !assertName.equals(name)) {
+ throw new IllegalArgumentException("The adapter defined in " +
+ c.getResources().getResourceEntryName(id) + " must be a <" + name + " />");
+ }
+
+ if (ADAPTER_CURSOR.equals(name)) {
+ adapter = createCursorAdapter(c, parser, attrs, id, parameters);
+ } else {
+ throw new IllegalArgumentException("Unknown adapter name " + parser.getName() +
+ " in " + c.getResources().getResourceEntryName(id));
+ }
+ }
+
+ return adapter;
+
+ }
+
+ /**
+ * Creates an XmlCursorAdapter using an XmlCursorAdapterParser.
+ */
+ private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser,
+ AttributeSet attrs, int id, Object[] parameters)
+ throws IOException, XmlPullParserException {
+
+ return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters);
+ }
+
+ /**
+ * Parser that can generate XmlCursorAdapter instances. This parser is responsible for
+ * handling all the attributes and child nodes for a &lt;cursor-adapter /&gt;.
+ */
+ private static class XmlCursorAdapterParser {
+ private static final String ADAPTER_CURSOR_BIND = "bind";
+ private static final String ADAPTER_CURSOR_SELECT = "select";
+ private static final String ADAPTER_CURSOR_AS_STRING = "string";
+ private static final String ADAPTER_CURSOR_AS_IMAGE = "image";
+ private static final String ADAPTER_CURSOR_AS_TAG = "tag";
+ private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri";
+ private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable";
+ private static final String ADAPTER_CURSOR_MAP = "map";
+ private static final String ADAPTER_CURSOR_TRANSFORM = "transform";
+
+ private final Context mContext;
+ private final XmlPullParser mParser;
+ private final AttributeSet mAttrs;
+ private final int mId;
+
+ private final HashMap<String, CursorBinder> mBinders;
+ private final ArrayList<String> mFrom;
+ private final ArrayList<Integer> mTo;
+ private final CursorTransformation mIdentity;
+ private final Resources mResources;
+
+ public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) {
+ mContext = c;
+ mParser = parser;
+ mAttrs = attrs;
+ mId = id;
+
+ mResources = mContext.getResources();
+ mBinders = new HashMap<String, CursorBinder>();
+ mFrom = new ArrayList<String>();
+ mTo = new ArrayList<Integer>();
+ mIdentity = new IdentityTransformation(mContext);
+ }
+
+ public XmlCursorAdapter parse(Object[] parameters)
+ throws IOException, XmlPullParserException {
+
+ Resources resources = mResources;
+ TypedArray a = resources.obtainAttributes(mAttrs, android.R.styleable.CursorAdapter);
+
+ String uri = a.getString(android.R.styleable.CursorAdapter_uri);
+ String selection = a.getString(android.R.styleable.CursorAdapter_selection);
+ String sortOrder = a.getString(android.R.styleable.CursorAdapter_sortOrder);
+ int layout = a.getResourceId(android.R.styleable.CursorAdapter_layout, 0);
+ if (layout == 0) {
+ throw new IllegalArgumentException("The layout specified in " +
+ resources.getResourceEntryName(mId) + " does not exist");
+ }
+
+ a.recycle();
+
+ XmlPullParser parser = mParser;
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
+ type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if (ADAPTER_CURSOR_BIND.equals(name)) {
+ parseBindTag();
+ } else if (ADAPTER_CURSOR_SELECT.equals(name)) {
+ parseSelectTag();
+ } else {
+ throw new RuntimeException("Unknown tag name " + parser.getName() + " in " +
+ resources.getResourceEntryName(mId));
+ }
+ }
+
+ String[] fromArray = mFrom.toArray(new String[mFrom.size()]);
+ int[] toArray = new int[mTo.size()];
+ for (int i = 0; i < toArray.length; i++) {
+ toArray[i] = mTo.get(i);
+ }
+
+ String[] selectionArgs = null;
+ if (parameters != null) {
+ selectionArgs = new String[parameters.length];
+ for (int i = 0; i < selectionArgs.length; i++) {
+ selectionArgs[i] = (String) parameters[i];
+ }
+ }
+
+ return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection,
+ selectionArgs, sortOrder, mBinders);
+ }
+
+ private void parseSelectTag() {
+ TypedArray a = mResources.obtainAttributes(mAttrs,
+ android.R.styleable.CursorAdapter_SelectItem);
+
+ String fromName = a.getString(android.R.styleable.CursorAdapter_SelectItem_column);
+ if (fromName == null) {
+ throw new IllegalArgumentException("A select item in " +
+ mResources.getResourceEntryName(mId) +
+ " does not have a 'column' attribute");
+ }
+
+ a.recycle();
+
+ mFrom.add(fromName);
+ mTo.add(View.NO_ID);
+ }
+
+ private void parseBindTag() throws IOException, XmlPullParserException {
+ Resources resources = mResources;
+ TypedArray a = resources.obtainAttributes(mAttrs,
+ android.R.styleable.CursorAdapter_BindItem);
+
+ String fromName = a.getString(android.R.styleable.CursorAdapter_BindItem_from);
+ if (fromName == null) {
+ throw new IllegalArgumentException("A bind item in " +
+ resources.getResourceEntryName(mId) + " does not have a 'from' attribute");
+ }
+
+ int toName = a.getResourceId(android.R.styleable.CursorAdapter_BindItem_to, 0);
+ if (toName == 0) {
+ throw new IllegalArgumentException("A bind item in " +
+ resources.getResourceEntryName(mId) + " does not have a 'to' attribute");
+ }
+
+ String asType = a.getString(android.R.styleable.CursorAdapter_BindItem_as);
+ if (asType == null) {
+ throw new IllegalArgumentException("A bind item in " +
+ resources.getResourceEntryName(mId) + " does not have an 'as' attribute");
+ }
+
+ mFrom.add(fromName);
+ mTo.add(toName);
+ mBinders.put(fromName, findBinder(asType));
+
+ a.recycle();
+ }
+
+ private CursorBinder findBinder(String type) throws IOException, XmlPullParserException {
+ final XmlPullParser parser = mParser;
+ final Context context = mContext;
+ CursorTransformation transformation = mIdentity;
+
+ int tagType;
+ int depth = parser.getDepth();
+
+ final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type);
+
+ while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && tagType != XmlPullParser.END_DOCUMENT) {
+
+ if (tagType != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if (ADAPTER_CURSOR_TRANSFORM.equals(name)) {
+ transformation = findTransformation();
+ } else if (ADAPTER_CURSOR_MAP.equals(name)) {
+ if (!(transformation instanceof MapTransformation)) {
+ transformation = new MapTransformation(context);
+ }
+ findMap(((MapTransformation) transformation), isDrawable);
+ } else {
+ throw new RuntimeException("Unknown tag name " + parser.getName() + " in " +
+ context.getResources().getResourceEntryName(mId));
+ }
+ }
+
+ if (ADAPTER_CURSOR_AS_STRING.equals(type)) {
+ return new StringBinder(context, transformation);
+ } else if (ADAPTER_CURSOR_AS_TAG.equals(type)) {
+ return new TagBinder(context, transformation);
+ } else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) {
+ return new ImageBinder(context, transformation);
+ } else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) {
+ return new ImageUriBinder(context, transformation);
+ } else if (isDrawable) {
+ return new DrawableBinder(context, transformation);
+ } else {
+ return createBinder(type, transformation);
+ }
+ }
+
+ private CursorBinder createBinder(String type, CursorTransformation transformation) {
+ if (mContext.isRestricted()) return null;
+
+ try {
+ final Class<?> klass = Class.forName(type, true, mContext.getClassLoader());
+ if (CursorBinder.class.isAssignableFrom(klass)) {
+ final Constructor<?> c = klass.getDeclaredConstructor(
+ Context.class, CursorTransformation.class);
+ return (CursorBinder) c.newInstance(mContext, transformation);
+ }
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Cannot instanciate binder type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot instanciate binder type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException("Cannot instanciate binder type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
+ } catch (InstantiationException e) {
+ throw new IllegalArgumentException("Cannot instanciate binder type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException("Cannot instanciate binder type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
+ }
+
+ return null;
+ }
+
+ private void findMap(MapTransformation transformation, boolean drawable) {
+ Resources resources = mResources;
+
+ TypedArray a = resources.obtainAttributes(mAttrs,
+ android.R.styleable.CursorAdapter_MapItem);
+
+ String from = a.getString(android.R.styleable.CursorAdapter_MapItem_fromValue);
+ if (from == null) {
+ throw new IllegalArgumentException("A map item in " +
+ resources.getResourceEntryName(mId) +
+ " does not have a 'fromValue' attribute");
+ }
+
+ if (!drawable) {
+ String to = a.getString(android.R.styleable.CursorAdapter_MapItem_toValue);
+ if (to == null) {
+ throw new IllegalArgumentException("A map item in " +
+ resources.getResourceEntryName(mId) +
+ " does not have a 'toValue' attribute");
+ }
+ transformation.addStringMapping(from, to);
+ } else {
+ int to = a.getResourceId(android.R.styleable.CursorAdapter_MapItem_toValue, 0);
+ if (to == 0) {
+ throw new IllegalArgumentException("A map item in " +
+ resources.getResourceEntryName(mId) +
+ " does not have a 'toValue' attribute");
+ }
+ transformation.addResourceMapping(from, to);
+ }
+
+ a.recycle();
+ }
+
+ private CursorTransformation findTransformation() {
+ Resources resources = mResources;
+ CursorTransformation transformation = null;
+ TypedArray a = resources.obtainAttributes(mAttrs,
+ android.R.styleable.CursorAdapter_TransformItem);
+
+ String className = a.getString(android.R.styleable.CursorAdapter_TransformItem_withClass);
+ if (className == null) {
+ String expression = a.getString(
+ android.R.styleable.CursorAdapter_TransformItem_withExpression);
+ transformation = createExpressionTransformation(expression);
+ } else if (!mContext.isRestricted()) {
+ try {
+ final Class<?> klas = Class.forName(className, true, mContext.getClassLoader());
+ if (CursorTransformation.class.isAssignableFrom(klas)) {
+ final Constructor<?> c = klas.getDeclaredConstructor(Context.class);
+ transformation = (CursorTransformation) c.newInstance(mContext);
+ }
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Cannot instanciate transform type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot instanciate transform type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException("Cannot instanciate transform type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
+ } catch (InstantiationException e) {
+ throw new IllegalArgumentException("Cannot instanciate transform type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException("Cannot instanciate transform type in " +
+ mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
+ }
+ }
+
+ a.recycle();
+
+ if (transformation == null) {
+ throw new IllegalArgumentException("A transform item in " +
+ resources.getResourceEntryName(mId) + " must have a 'withClass' or " +
+ "'withExpression' attribute");
+ }
+
+ return transformation;
+ }
+
+ private CursorTransformation createExpressionTransformation(String expression) {
+ return new ExpressionTransformation(mContext, expression);
+ }
+ }
+
+ /**
+ * Interface used by adapters that require to be loaded after creation.
+ */
+ private static interface ManagedAdapter {
+ /**
+ * Loads the content of the adapter, asynchronously.
+ */
+ void load();
+ }
+
+ /**
+ * Implementation of a Cursor adapter defined in XML. This class is a thin wrapper
+ * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders.
+ */
+ private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter {
+ private String mUri;
+ private final String mSelection;
+ private final String[] mSelectionArgs;
+ private final String mSortOrder;
+ private final String[] mColumns;
+ private final CursorBinder[] mBinders;
+ private AsyncTask<Void,Void,Cursor> mLoadTask;
+
+ XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to,
+ String selection, String[] selectionArgs, String sortOrder,
+ HashMap<String, CursorBinder> binders) {
+
+ super(context, layout, null, from, to);
+ mContext = context;
+ mUri = uri;
+ mSelection = selection;
+ mSelectionArgs = selectionArgs;
+ mSortOrder = sortOrder;
+ mColumns = new String[from.length + 1];
+ // This is mandatory in CursorAdapter
+ mColumns[0] = "_id";
+ System.arraycopy(from, 0, mColumns, 1, from.length);
+
+ CursorBinder basic = new StringBinder(context, new IdentityTransformation(context));
+ final int count = from.length;
+ mBinders = new CursorBinder[count];
+
+ for (int i = 0; i < count; i++) {
+ CursorBinder binder = binders.get(from[i]);
+ if (binder == null) binder = basic;
+ mBinders[i] = binder;
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final int count = mTo.length;
+ final int[] from = mFrom;
+ final int[] to = mTo;
+ final CursorBinder[] binders = mBinders;
+
+ for (int i = 0; i < count; i++) {
+ final View v = view.findViewById(to[i]);
+ if (v != null) {
+ binders[i].bind(v, cursor, from[i]);
+ }
+ }
+ }
+
+ public void load() {
+ if (mUri != null) {
+ mLoadTask = new QueryTask().execute();
+ }
+ }
+
+ void setUri(String uri) {
+ mUri = uri;
+ }
+
+ @Override
+ public void changeCursor(Cursor c) {
+ if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) {
+ mLoadTask.cancel(true);
+ mLoadTask = null;
+ }
+ super.changeCursor(c);
+ }
+
+ class QueryTask extends AsyncTask<Void, Void, Cursor> {
+ @Override
+ protected Cursor doInBackground(Void... params) {
+ if (mContext instanceof Activity) {
+ return ((Activity) mContext).managedQuery(
+ Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder);
+ } else {
+ return mContext.getContentResolver().query(
+ Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Cursor cursor) {
+ if (!isCancelled()) {
+ XmlCursorAdapter.super.changeCursor(cursor);
+ }
+ }
+ }
+ }
+
+ /**
+ * Identity transformation, returns the content of the specified column as a String,
+ * without performing any manipulation. This is used when no transformation is specified.
+ */
+ private static class IdentityTransformation extends CursorTransformation {
+ public IdentityTransformation(Context context) {
+ super(context);
+ }
+
+ @Override
+ public String transform(Cursor cursor, int columnIndex) {
+ return cursor.getString(columnIndex);
+ }
+ }
+
+ /**
+ * An expression transformation is a simple template based replacement utility.
+ * In an expression, each segment of the form <code>{([^}]+)}</code> is replaced
+ * with the value of the column of name $1.
+ */
+ private static class ExpressionTransformation extends CursorTransformation {
+ private final ExpressionNode mFirstNode = new ConstantExpressionNode("");
+ private final StringBuilder mBuilder = new StringBuilder();
+
+ public ExpressionTransformation(Context context, String expression) {
+ super(context);
+
+ parse(expression);
+ }
+
+ private void parse(String expression) {
+ ExpressionNode node = mFirstNode;
+ int segmentStart;
+ int count = expression.length();
+
+ for (int i = 0; i < count; i++) {
+ char c = expression.charAt(i);
+ // Start a column name segment
+ segmentStart = i;
+ if (c == '{') {
+ while (i < count && (c = expression.charAt(i)) != '}') {
+ i++;
+ }
+ // We've reached the end, but the expression didn't close
+ if (c != '}') {
+ throw new IllegalStateException("The transform expression contains a " +
+ "non-closed column name: " +
+ expression.substring(segmentStart + 1, i));
+ }
+ node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i));
+ } else {
+ while (i < count && (c = expression.charAt(i)) != '{') {
+ i++;
+ }
+ node.next = new ConstantExpressionNode(expression.substring(segmentStart, i));
+ // Rewind if we've reached a column expression
+ if (c == '{') i--;
+ }
+ node = node.next;
+ }
+ }
+
+ @Override
+ public String transform(Cursor cursor, int columnIndex) {
+ final StringBuilder builder = mBuilder;
+ builder.delete(0, builder.length());
+
+ ExpressionNode node = mFirstNode;
+ // Skip the first node
+ while ((node = node.next) != null) {
+ builder.append(node.asString(cursor));
+ }
+
+ return builder.toString();
+ }
+
+ static abstract class ExpressionNode {
+ public ExpressionNode next;
+
+ public abstract String asString(Cursor cursor);
+ }
+
+ static class ConstantExpressionNode extends ExpressionNode {
+ private final String mConstant;
+
+ ConstantExpressionNode(String constant) {
+ mConstant = constant;
+ }
+
+ @Override
+ public String asString(Cursor cursor) {
+ return mConstant;
+ }
+ }
+
+ static class ColumnExpressionNode extends ExpressionNode {
+ private final String mColumnName;
+ private Cursor mSignature;
+ private int mColumnIndex = -1;
+
+ ColumnExpressionNode(String columnName) {
+ mColumnName = columnName;
+ }
+
+ @Override
+ public String asString(Cursor cursor) {
+ if (cursor != mSignature || mColumnIndex == -1) {
+ mColumnIndex = cursor.getColumnIndex(mColumnName);
+ mSignature = cursor;
+ }
+
+ return cursor.getString(mColumnIndex);
+ }
+ }
+ }
+
+ /**
+ * A map transformation offers a simple mapping between specified String values
+ * to Strings or integers.
+ */
+ private static class MapTransformation extends CursorTransformation {
+ private final HashMap<String, String> mStringMappings;
+ private final HashMap<String, Integer> mResourceMappings;
+
+ public MapTransformation(Context context) {
+ super(context);
+ mStringMappings = new HashMap<String, String>();
+ mResourceMappings = new HashMap<String, Integer>();
+ }
+
+ void addStringMapping(String from, String to) {
+ mStringMappings.put(from, to);
+ }
+
+ void addResourceMapping(String from, int to) {
+ mResourceMappings.put(from, to);
+ }
+
+ @Override
+ public String transform(Cursor cursor, int columnIndex) {
+ final String value = cursor.getString(columnIndex);
+ final String transformed = mStringMappings.get(value);
+ return transformed == null ? value : transformed;
+ }
+
+ @Override
+ public int transformToResource(Cursor cursor, int columnIndex) {
+ final String value = cursor.getString(columnIndex);
+ final Integer transformed = mResourceMappings.get(value);
+ try {
+ return transformed == null ? Integer.parseInt(value) : transformed;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+ }
+
+ /**
+ * Binds a String to a TextView.
+ */
+ private static class StringBinder extends CursorBinder {
+ public StringBinder(Context context, CursorTransformation transformation) {
+ super(context, transformation);
+ }
+
+ @Override
+ public boolean bind(View view, Cursor cursor, int columnIndex) {
+ if (view instanceof TextView) {
+ final String text = mTransformation.transform(cursor, columnIndex);
+ ((TextView) view).setText(text);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Binds an image blob to an ImageView.
+ */
+ private static class ImageBinder extends CursorBinder {
+ public ImageBinder(Context context, CursorTransformation transformation) {
+ super(context, transformation);
+ }
+
+ @Override
+ public boolean bind(View view, Cursor cursor, int columnIndex) {
+ if (view instanceof ImageView) {
+ final byte[] data = cursor.getBlob(columnIndex);
+ ((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0,
+ data.length));
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private static class TagBinder extends CursorBinder {
+ public TagBinder(Context context, CursorTransformation transformation) {
+ super(context, transformation);
+ }
+
+ @Override
+ public boolean bind(View view, Cursor cursor, int columnIndex) {
+ final String text = mTransformation.transform(cursor, columnIndex);
+ view.setTag(text);
+ return true;
+ }
+ }
+
+ /**
+ * Binds an image URI to an ImageView.
+ */
+ private static class ImageUriBinder extends CursorBinder {
+ public ImageUriBinder(Context context, CursorTransformation transformation) {
+ super(context, transformation);
+ }
+
+ @Override
+ public boolean bind(View view, Cursor cursor, int columnIndex) {
+ if (view instanceof ImageView) {
+ ((ImageView) view).setImageURI(Uri.parse(
+ mTransformation.transform(cursor, columnIndex)));
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Binds a drawable resource identifier to an ImageView.
+ */
+ private static class DrawableBinder extends CursorBinder {
+ public DrawableBinder(Context context, CursorTransformation transformation) {
+ super(context, transformation);
+ }
+
+ @Override
+ public boolean bind(View view, Cursor cursor, int columnIndex) {
+ if (view instanceof ImageView) {
+ final int resource = mTransformation.transformToResource(cursor, columnIndex);
+ if (resource == 0) return false;
+
+ ((ImageView) view).setImageResource(resource);
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
index e15a520..34aef99 100644
--- a/core/java/android/widget/AutoCompleteTextView.java
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -29,13 +29,12 @@ import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
import com.android.internal.R;
@@ -90,45 +89,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
static final boolean DEBUG = false;
static final String TAG = "AutoCompleteTextView";
- private static final int HINT_VIEW_ID = 0x17;
-
- /**
- * This value controls the length of time that the user
- * must leave a pointer down without scrolling to expand
- * the autocomplete dropdown list to cover the IME.
- */
- private static final int EXPAND_LIST_TIMEOUT = 250;
-
private CharSequence mHintText;
+ private TextView mHintView;
private int mHintResource;
private ListAdapter mAdapter;
private Filter mFilter;
private int mThreshold;
- private PopupWindow mPopup;
- private DropDownListView mDropDownList;
- private int mDropDownVerticalOffset;
- private int mDropDownHorizontalOffset;
+ private ListPopupWindow mPopup;
private int mDropDownAnchorId;
- private View mDropDownAnchorView; // view is retrieved lazily from id once needed
- private int mDropDownWidth;
- private int mDropDownHeight;
- private final Rect mTempRect = new Rect();
-
- private Drawable mDropDownListHighlight;
private AdapterView.OnItemClickListener mItemClickListener;
private AdapterView.OnItemSelectedListener mItemSelectedListener;
- private final DropDownItemClickListener mDropDownItemClickListener =
- new DropDownItemClickListener();
-
- private boolean mDropDownAlwaysVisible = false;
-
private boolean mDropDownDismissedOnCompletion = true;
-
- private boolean mForceIgnoreOutsideTouch = false;
private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
private boolean mOpenBefore;
@@ -137,10 +112,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
private boolean mBlockCompletion;
- private ListSelectorHider mHideSelector;
- private Runnable mShowDropDownRunnable;
- private Runnable mResizePopupRunnable = new ResizePopupRunnable();
-
private PassThroughClickListener mPassThroughClickListener;
private PopupDataSetObserver mObserver;
@@ -155,9 +126,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mPopup = new PopupWindow(context, attrs,
+ mPopup = new ListPopupWindow(context, attrs,
com.android.internal.R.attr.autoCompleteTextViewStyle);
mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+ mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
TypedArray a =
context.obtainStyledAttributes(
@@ -166,14 +138,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
mThreshold = a.getInt(
R.styleable.AutoCompleteTextView_completionThreshold, 2);
- mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint);
-
- mDropDownListHighlight = a.getDrawable(
- R.styleable.AutoCompleteTextView_dropDownSelector);
- mDropDownVerticalOffset = (int)
- a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f);
- mDropDownHorizontalOffset = (int)
- a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f);
+ mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector));
+ mPopup.setVerticalOffset((int)
+ a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f));
+ mPopup.setHorizontalOffset((int)
+ a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f));
// Get the anchor's id now, but the view won't be ready, so wait to actually get the
// view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
@@ -184,13 +153,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
// For dropdown width, the developer can specify a specific width, or MATCH_PARENT
// (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
- mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight,
- ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopup.setWidth(a.getLayoutDimension(
+ R.styleable.AutoCompleteTextView_dropDownWidth,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ mPopup.setHeight(a.getLayoutDimension(
+ R.styleable.AutoCompleteTextView_dropDownHeight,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
R.layout.simple_dropdown_hint);
+
+ mPopup.setOnItemClickListener(new DropDownItemClickListener());
+ setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint));
// Always turn on the auto complete input type flag, since it
// makes no sense to use this widget without it.
@@ -238,6 +212,20 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
*/
public void setCompletionHint(CharSequence hint) {
mHintText = hint;
+ if (hint != null) {
+ if (mHintView == null) {
+ final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate(
+ mHintResource, null).findViewById(com.android.internal.R.id.text1);
+ hintView.setText(mHintText);
+ mHintView = hintView;
+ mPopup.setPromptView(hintView);
+ } else {
+ mHintView.setText(hint);
+ }
+ } else {
+ mPopup.setPromptView(null);
+ mHintView = null;
+ }
}
/**
@@ -250,7 +238,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
*/
public int getDropDownWidth() {
- return mDropDownWidth;
+ return mPopup.getWidth();
}
/**
@@ -263,7 +251,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
*/
public void setDropDownWidth(int width) {
- mDropDownWidth = width;
+ mPopup.setWidth(width);
}
/**
@@ -277,7 +265,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
*/
public int getDropDownHeight() {
- return mDropDownHeight;
+ return mPopup.getHeight();
}
/**
@@ -291,7 +279,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
*/
public void setDropDownHeight(int height) {
- mDropDownHeight = height;
+ mPopup.setHeight(height);
}
/**
@@ -316,7 +304,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
*/
public void setDropDownAnchor(int id) {
mDropDownAnchorId = id;
- mDropDownAnchorView = null;
+ mPopup.setAnchorView(null);
}
/**
@@ -358,7 +346,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @param offset the vertical offset
*/
public void setDropDownVerticalOffset(int offset) {
- mDropDownVerticalOffset = offset;
+ mPopup.setVerticalOffset(offset);
}
/**
@@ -367,7 +355,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @return the vertical offset
*/
public int getDropDownVerticalOffset() {
- return mDropDownVerticalOffset;
+ return mPopup.getVerticalOffset();
}
/**
@@ -376,7 +364,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @param offset the horizontal offset
*/
public void setDropDownHorizontalOffset(int offset) {
- mDropDownHorizontalOffset = offset;
+ mPopup.setHorizontalOffset(offset);
}
/**
@@ -385,7 +373,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @return the horizontal offset
*/
public int getDropDownHorizontalOffset() {
- return mDropDownHorizontalOffset;
+ return mPopup.getHorizontalOffset();
}
/**
@@ -422,7 +410,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @hide Pending API council approval
*/
public boolean isDropDownAlwaysVisible() {
- return mDropDownAlwaysVisible;
+ return mPopup.isDropDownAlwaysVisible();
}
/**
@@ -439,7 +427,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @hide Pending API council approval
*/
public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
- mDropDownAlwaysVisible = dropDownAlwaysVisible;
+ mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible);
}
/**
@@ -606,15 +594,13 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
mFilter = null;
}
- if (mDropDownList != null) {
- mDropDownList.setAdapter(mAdapter);
- }
+ mPopup.setAdapter(mAdapter);
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
- && !mDropDownAlwaysVisible) {
+ && !mPopup.isDropDownAlwaysVisible()) {
// 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 (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
@@ -633,18 +619,16 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
- boolean consumed = mDropDownList.onKeyUp(keyCode, event);
- if (consumed) {
- switch (keyCode) {
- // if the list accepts the key events and the key event
- // was a click, the text view gets the selected item
- // from the drop down as its content
- case KeyEvent.KEYCODE_ENTER:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- performCompletion();
- return true;
- }
+ boolean consumed = mPopup.onKeyUp(keyCode, event);
+ if (consumed) {
+ switch (keyCode) {
+ // if the list accepts the key events and the key event
+ // was a click, the text view gets the selected item
+ // from the drop down as its content
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ performCompletion();
+ return true;
}
}
return super.onKeyUp(keyCode, event);
@@ -652,87 +636,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- // when the drop down is shown, we drive it directly
- if (isPopupShowing()) {
- // the key events are forwarded to the list in the drop down view
- // note that ListView handles space but we don't want that to happen
- // also if selection is not currently in the drop down, then don't
- // let center or enter presses go there since that would cause it
- // to select one of its items
- if (keyCode != KeyEvent.KEYCODE_SPACE
- && (mDropDownList.getSelectedItemPosition() >= 0
- || (keyCode != KeyEvent.KEYCODE_ENTER
- && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) {
- int curIndex = mDropDownList.getSelectedItemPosition();
- boolean consumed;
-
- final boolean below = !mPopup.isAboveAnchor();
-
- final ListAdapter adapter = mAdapter;
-
- boolean allEnabled;
- int firstItem = Integer.MAX_VALUE;
- int lastItem = Integer.MIN_VALUE;
-
- if (adapter != null) {
- allEnabled = adapter.areAllItemsEnabled();
- firstItem = allEnabled ? 0 :
- mDropDownList.lookForSelectablePosition(0, true);
- lastItem = allEnabled ? adapter.getCount() - 1 :
- mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
- }
-
- if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
- (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
- // When the selection is at the top, we block the key
- // event to prevent focus from moving.
- clearListSelection();
- mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
- 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 (consumed) {
- // If it handled the key event, then the user is
- // navigating in the list, so we should put it in front.
- mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
- // Here's a little trick we need to do to make sure that
- // the list view is actually showing its focus indicator,
- // by ensuring it has focus and getting its window out
- // of touch mode.
- mDropDownList.requestFocusFromTouch();
- showDropDown();
-
- switch (keyCode) {
- // avoid passing the focus from the text view to the
- // next component
- case KeyEvent.KEYCODE_ENTER:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_UP:
- return true;
- }
- } else {
- if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
- // when the selection is at the bottom, we block the
- // event to avoid going to the next focusable widget
- if (curIndex == lastItem) {
- return true;
- }
- } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
- curIndex == firstItem) {
- return true;
- }
- }
- }
- } else {
+ if (mPopup.onKeyDown(keyCode, event)) {
+ return true;
+ }
+
+ if (!isPopupShowing()) {
switch(keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
performValidation();
@@ -743,7 +651,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
boolean handled = super.onKeyDown(keyCode, event);
mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
- if (handled && isPopupShowing() && mDropDownList != null) {
+ if (handled && isPopupShowing()) {
clearListSelection();
}
@@ -804,11 +712,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
if (enoughToFilter()) {
if (mFilter != null) {
performFiltering(getText(), mLastKeyCode);
+ buildImeCompletions();
}
} else {
// drop down is automatically dismissed when enough characters
// are deleted from the text view
- if (!mDropDownAlwaysVisible) dismissDropDown();
+ if (!mPopup.isDropDownAlwaysVisible()) dismissDropDown();
if (mFilter != null) {
mFilter.filter(null);
}
@@ -841,13 +750,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* it back.
*/
public void clearListSelection() {
- final DropDownListView list = mDropDownList;
- if (list != null) {
- // WARNING: Please read the comment where mListSelectionHidden is declared
- list.mListSelectionHidden = true;
- list.hideSelector();
- list.requestLayout();
- }
+ mPopup.clearListSelection();
}
/**
@@ -856,11 +759,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @param position The position to move the selector to.
*/
public void setListSelection(int position) {
- if (mPopup.isShowing() && (mDropDownList != null)) {
- mDropDownList.mListSelectionHidden = false;
- mDropDownList.setSelection(position);
- // ListView.setSelection() will call requestLayout()
- }
+ mPopup.setSelection(position);
}
/**
@@ -874,10 +773,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @see ListView#getSelectedItemPosition()
*/
public int getListSelection() {
- if (mPopup.isShowing() && (mDropDownList != null)) {
- return mDropDownList.getSelectedItemPosition();
- }
- return ListView.INVALID_POSITION;
+ return mPopup.getSelectedItemPosition();
}
/**
@@ -911,13 +807,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
replaceText(completion.getText());
mBlockCompletion = false;
- if (mItemClickListener != null) {
- final DropDownListView list = mDropDownList;
- // Note that we don't have a View here, so we will need to
- // supply null. Hopefully no existing apps crash...
- mItemClickListener.onItemClick(list, null, completion.getPosition(),
- completion.getId());
- }
+ mPopup.performItemClick(completion.getPosition());
}
}
@@ -925,7 +815,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
if (isPopupShowing()) {
Object selectedItem;
if (position < 0) {
- selectedItem = mDropDownList.getSelectedItem();
+ selectedItem = mPopup.getSelectedItem();
} else {
selectedItem = mAdapter.getItem(position);
}
@@ -939,18 +829,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
mBlockCompletion = false;
if (mItemClickListener != null) {
- final DropDownListView list = mDropDownList;
+ final ListPopupWindow list = mPopup;
if (selectedView == null || position < 0) {
selectedView = list.getSelectedView();
position = list.getSelectedItemPosition();
id = list.getSelectedItemId();
}
- mItemClickListener.onItemClick(list, selectedView, position, id);
+ mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
}
}
- if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) {
+ if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
dismissDropDown();
}
}
@@ -1000,7 +890,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
/** {@inheritDoc} */
public void onFilterComplete(int count) {
updateDropDownForFilter(count);
-
}
private void updateDropDownForFilter(int count) {
@@ -1014,11 +903,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* to filter.
*/
- if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) {
+ final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
+ if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter()) {
if (hasFocus() && hasWindowFocus()) {
showDropDown();
}
- } else if (!mDropDownAlwaysVisible) {
+ } else if (!dropDownAlwaysVisible) {
dismissDropDown();
}
}
@@ -1026,7 +916,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
- if (!hasWindowFocus && !mDropDownAlwaysVisible) {
+ if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
dismissDropDown();
}
}
@@ -1036,7 +926,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
super.onDisplayHint(hint);
switch (hint) {
case INVISIBLE:
- if (!mDropDownAlwaysVisible) {
+ if (!mPopup.isDropDownAlwaysVisible()) {
dismissDropDown();
}
break;
@@ -1050,7 +940,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
if (!focused) {
performValidation();
}
- if (!focused && !mDropDownAlwaysVisible) {
+ if (!focused && !mPopup.isDropDownAlwaysVisible()) {
dismissDropDown();
}
}
@@ -1075,8 +965,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
imm.displayCompletions(this, null);
}
mPopup.dismiss();
- mPopup.setContentView(null);
- mDropDownList = null;
}
@Override
@@ -1089,18 +977,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
return result;
}
-
- /**
- * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of
- * the id is NO_ID or we can't find a view for the given id, we return this TextView as
- * the default anchoring point.</p>
- */
- private View getDropDownAnchorView() {
- if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) {
- mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId);
- }
- return mDropDownAnchorView == null ? this : mDropDownAnchorView;
- }
/**
* Issues a runnable to show the dropdown as soon as possible.
@@ -1108,7 +984,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @hide internal used only by SearchDialog
*/
public void showDropDownAfterLayout() {
- post(mShowDropDownRunnable);
+ mPopup.postShow();
}
/**
@@ -1119,7 +995,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
*/
public void ensureImeVisible(boolean visible) {
mPopup.setInputMethodMode(visible
- ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
showDropDown();
}
@@ -1127,89 +1003,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @hide internal used only here and SearchDialog
*/
public boolean isInputMethodNotNeeded() {
- return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
+ return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED;
}
/**
* <p>Displays the drop down on screen.</p>
*/
public void showDropDown() {
- int height = buildDropDown();
-
- int widthSpec = 0;
- int heightSpec = 0;
-
- boolean noInputMethod = isInputMethodNotNeeded();
-
- if (mPopup.isShowing()) {
- if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
- // The call to PopupWindow's update method below can accept -1 for any
- // value you do not want to update.
- widthSpec = -1;
- } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
- widthSpec = getDropDownAnchorView().getWidth();
- } else {
- widthSpec = mDropDownWidth;
- }
-
- if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_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.MATCH_PARENT;
- if (noInputMethod) {
- mPopup.setWindowLayoutMode(
- mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
- ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
- } else {
- mPopup.setWindowLayoutMode(
- mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
- ViewGroup.LayoutParams.MATCH_PARENT : 0,
- ViewGroup.LayoutParams.MATCH_PARENT);
- }
- } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
- heightSpec = height;
+ if (mPopup.getAnchorView() == null) {
+ if (mDropDownAnchorId != View.NO_ID) {
+ mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
} else {
- heightSpec = mDropDownHeight;
+ mPopup.setAnchorView(this);
}
-
- mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
-
- mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset,
- mDropDownVerticalOffset, widthSpec, heightSpec);
- } else {
- if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
- widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
- } else {
- if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
- mPopup.setWidth(getDropDownAnchorView().getWidth());
- } else {
- mPopup.setWidth(mDropDownWidth);
- }
- }
-
- if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
- heightSpec = ViewGroup.LayoutParams.MATCH_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
- // only set this if the dropdown is not always visible
- mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
- mPopup.setTouchInterceptor(new PopupTouchInterceptor());
- mPopup.showAsDropDown(getDropDownAnchorView(),
- mDropDownHorizontalOffset, mDropDownVerticalOffset);
- mDropDownList.setSelection(ListView.INVALID_POSITION);
- clearListSelection();
- post(mHideSelector);
}
+ mPopup.show();
}
/**
@@ -1220,19 +1028,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* @hide used only by SearchDialog
*/
public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
- mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
+ mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch);
}
-
- /**
- * <p>Builds the popup window's content and returns the height the popup
- * should have. Returns -1 when the content already exists.</p>
- *
- * @return the content's height or -1 if content already exists
- */
- private int buildDropDown() {
- ViewGroup dropDownView;
- int otherHeights = 0;
-
+
+ private void buildImeCompletions() {
final ListAdapter adapter = mAdapter;
if (adapter != null) {
InputMethodManager imm = InputMethodManager.peekInstance();
@@ -1260,135 +1059,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
imm.displayCompletions(this, completions);
}
}
-
- if (mDropDownList == null) {
- Context context = getContext();
-
- 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(adapter);
- mDropDownList.setVerticalFadingEdgeEnabled(true);
- 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) {
- }
- });
- mDropDownList.setOnScrollListener(new PopupScrollListener());
-
- if (mItemSelectedListener != null) {
- mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
- }
-
- dropDownView = mDropDownList;
-
- View hintView = getHintView(context);
- if (hintView != null) {
- // if an hint has been specified, we accomodate more space for it and
- // add a text view in the drop down menu, at the bottom of the list
- LinearLayout hintContainer = new LinearLayout(context);
- hintContainer.setOrientation(LinearLayout.VERTICAL);
-
- LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
- );
- hintContainer.addView(dropDownView, hintParams);
- hintContainer.addView(hintView);
-
- // measure the hint's height to find how much more vertical space
- // we need to add to the drop down's height
- int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST);
- int heightSpec = MeasureSpec.UNSPECIFIED;
- hintView.measure(widthSpec, heightSpec);
-
- hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
- otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
- + hintParams.bottomMargin;
-
- dropDownView = hintContainer;
- }
-
- mPopup.setContentView(dropDownView);
- } else {
- dropDownView = (ViewGroup) mPopup.getContentView();
- final View view = dropDownView.findViewById(HINT_VIEW_ID);
- if (view != null) {
- LinearLayout.LayoutParams hintParams =
- (LinearLayout.LayoutParams) view.getLayoutParams();
- otherHeights = view.getMeasuredHeight() + hintParams.topMargin
- + hintParams.bottomMargin;
- }
- }
-
- // Max height available on the screen for a popup.
- boolean ignoreBottomDecorations =
- mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
- final int maxHeight = mPopup.getMaxAvailableHeight(
- getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
-
- // getMaxAvailableHeight() subtracts the padding, so we put it back,
- // to get the available height for the whole window
- int padding = 0;
- Drawable background = mPopup.getBackground();
- if (background != null) {
- background.getPadding(mTempRect);
- padding = mTempRect.top + mTempRect.bottom;
- }
-
- if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
- return maxHeight + padding;
- }
-
- final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
- 0, ListView.NO_POSITION, maxHeight - otherHeights, 2);
- // add padding only if the list has items in it, that way we don't show
- // the popup if it is not needed
- if (listContent > 0) otherHeights += padding;
-
- return listContent + otherHeights;
- }
-
- private View getHintView(Context context) {
- if (mHintText != null && mHintText.length() > 0) {
- final TextView hintView = (TextView) LayoutInflater.from(context).inflate(
- mHintResource, null).findViewById(com.android.internal.R.id.text1);
- hintView.setText(mHintText);
- hintView.setId(HINT_VIEW_ID);
- return hintView;
- } else {
- return null;
- }
}
/**
@@ -1440,47 +1110,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
return mFilter;
}
- private class ListSelectorHider implements Runnable {
- public void run() {
- clearListSelection();
- }
- }
-
- private class ResizePopupRunnable implements Runnable {
- public void run() {
- mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
- showDropDown();
- }
- }
-
- private class PopupTouchInterceptor implements OnTouchListener {
- public boolean onTouch(View v, MotionEvent event) {
- final int action = event.getAction();
- if (action == MotionEvent.ACTION_DOWN &&
- mPopup != null && mPopup.isShowing()) {
- postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
- } else if (action == MotionEvent.ACTION_UP) {
- removeCallbacks(mResizePopupRunnable);
- }
- return false;
- }
- }
-
- private class PopupScrollListener implements ListView.OnScrollListener {
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
- int totalItemCount) {
-
- }
-
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
- !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
- removeCallbacks(mResizePopupRunnable);
- mResizePopupRunnable.run();
- }
- }
- }
-
private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
public void onItemClick(AdapterView parent, View v, int position, long id) {
performCompletion(v, position, id);
@@ -1488,123 +1117,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
/**
- * <p>Wrapper class for a ListView. This wrapper hijacks the focus to
- * make sure the list uses the appropriate drawables and states when
- * displayed on screen within a drop down. The focus is never actually
- * 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>
- *
- * @param context this view's context
- */
- public DropDownListView(Context context) {
- super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
- }
-
- /**
- * <p>Avoids jarring scrolling effect by ensuring that list elements
- * made of a text view fit on a single line.</p>
- *
- * @param position the item index in the list to get a view for
- * @return the view for the specified item
- */
- @Override
- View obtainView(int position, boolean[] isScrap) {
- View view = super.obtainView(position, isScrap);
-
- if (view instanceof TextView) {
- ((TextView) view).setHorizontallyScrolling(true);
- }
-
- return view;
- }
-
- @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>
- *
- * @return true always
- */
- @Override
- public boolean hasWindowFocus() {
- return true;
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always
- */
- @Override
- public boolean isFocused() {
- return true;
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always
- */
- @Override
- public boolean hasFocus() {
- return true;
- }
-
- protected int[] onCreateDrawableState(int extraSpace) {
- int[] res = super.onCreateDrawableState(extraSpace);
- //noinspection ConstantIfStatement
- if (false) {
- StringBuilder sb = new StringBuilder("Created drawable state: [");
- for (int i=0; i<res.length; i++) {
- if (i > 0) sb.append(", ");
- sb.append("0x");
- sb.append(Integer.toHexString(res[i]));
- }
- sb.append("]");
- Log.i(TAG, sb.toString());
- }
- return res;
- }
- }
-
- /**
* This interface is used to make sure that the text entered in this TextView complies to
* a certain format. Since there is no foolproof way to prevent the user from leaving
* this View with an incorrect value in it, all we can do is try to fix it ourselves
@@ -1652,10 +1164,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
private class PopupDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
- if (isPopupShowing()) {
- // This will resize the popup to fit the new adapter's content
- showDropDown();
- } else if (mAdapter != null) {
+ if (mAdapter != null) {
// If the popup is not showing already, showing it will cause
// the list of data set observers attached to the adapter to
// change. We can't do it from here, because we are in the middle
@@ -1670,14 +1179,5 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
});
}
}
-
- @Override
- public void onInvalidated() {
- if (!mDropDownAlwaysVisible) {
- // There's no data to display so make sure we're not showing
- // the drop down and its list
- dismissDropDown();
- }
- }
}
}
diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java
index baa6833..4cf8785 100644
--- a/core/java/android/widget/CursorAdapter.java
+++ b/core/java/android/widget/CursorAdapter.java
@@ -80,6 +80,18 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable,
protected FilterQueryProvider mFilterQueryProvider;
/**
+ * If set the adapter will call requery() on the cursor whenever a content change
+ * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}
+ */
+ public static final int FLAG_AUTO_REQUERY = 0x01;
+
+ /**
+ * If set the adapter will register a content observer on the cursor and will call
+ * {@link #onContentChanged()} when a notification comes in.
+ */
+ public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
+
+ /**
* Constructor. The adapter will call requery() on the cursor whenever
* it changes so that the most recent data is always displayed.
*
@@ -87,7 +99,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable,
* @param context The context
*/
public CursorAdapter(Context context, Cursor c) {
- init(context, c, true);
+ init(context, c, FLAG_AUTO_REQUERY);
}
/**
@@ -99,19 +111,43 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable,
* data is always displayed.
*/
public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
- init(context, c, autoRequery);
+ init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
+ }
+
+ /**
+ * Constructor
+ * @param c The cursor from which to get the data.
+ * @param context The context
+ * @param flags flags used to determine the behavior of the adapter
+ */
+ public CursorAdapter(Context context, Cursor c, int flags) {
+ init(context, c, flags);
}
protected void init(Context context, Cursor c, boolean autoRequery) {
+ init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
+ }
+
+ protected void init(Context context, Cursor c, int flags) {
+ if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
+ flags |= FLAG_REGISTER_CONTENT_OBSERVER;
+ mAutoRequery = true;
+ } else {
+ mAutoRequery = false;
+ }
boolean cursorPresent = c != null;
- mAutoRequery = autoRequery;
mCursor = c;
mDataValid = cursorPresent;
mContext = context;
mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
- mChangeObserver = new ChangeObserver();
+ if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
+ mChangeObserver = new ChangeObserver();
+ } else {
+ mChangeObserver = null;
+ }
+
if (cursorPresent) {
- c.registerContentObserver(mChangeObserver);
+ if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
c.registerDataSetObserver(mDataSetObserver);
}
}
@@ -246,13 +282,13 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable,
return;
}
if (mCursor != null) {
- mCursor.unregisterContentObserver(mChangeObserver);
+ if (mChangeObserver != null) mCursor.unregisterContentObserver(mChangeObserver);
mCursor.unregisterDataSetObserver(mDataSetObserver);
mCursor.close();
}
mCursor = cursor;
if (cursor != null) {
- cursor.registerContentObserver(mChangeObserver);
+ if (mChangeObserver != null) cursor.registerContentObserver(mChangeObserver);
cursor.registerDataSetObserver(mDataSetObserver);
mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
mDataValid = true;
diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java
index 1ed6b16..c47292f 100644
--- a/core/java/android/widget/Gallery.java
+++ b/core/java/android/widget/Gallery.java
@@ -1207,7 +1207,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList
// We unfocus the old child down here so the above hasFocus check
// returns true
- if (oldSelectedChild != null) {
+ if (oldSelectedChild != null && oldSelectedChild != child) {
// Make sure its drawable state doesn't contain 'selected'
oldSelectedChild.setSelected(false);
@@ -1263,6 +1263,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList
*/
if (gainFocus && mSelectedChild != null) {
mSelectedChild.requestFocus(direction);
+ mSelectedChild.setSelected(true);
}
}
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index d2829db..fe69a13 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -23,6 +23,7 @@ import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
+import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.SoundEffectConstants;
import android.view.animation.GridLayoutAnimationController;
@@ -112,7 +113,7 @@ public class GridView extends AbsListView {
*/
@Override
public void setAdapter(ListAdapter adapter) {
- if (null != mAdapter) {
+ if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
@@ -1774,6 +1775,19 @@ public class GridView extends AbsListView {
requestLayoutIfNecessary();
}
}
+
+ /**
+ * Get the number of columns in the grid.
+ * Returns {@link #AUTO_FIT} if the Grid has never been laid out.
+ *
+ * @attr ref android.R.styleable#GridView_numColumns
+ *
+ * @see #setNumColumns(int)
+ */
+ @ViewDebug.ExportedProperty
+ public int getNumColumns() {
+ return mNumColumns;
+ }
/**
* Make sure views are touching the top or bottom edge, as appropriate for
diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java
index bd07e1f..7254c3c 100644
--- a/core/java/android/widget/LinearLayout.java
+++ b/core/java/android/widget/LinearLayout.java
@@ -40,6 +40,13 @@ import android.widget.RemoteViews.RemoteView;
* <p>
* Also see {@link LinearLayout.LayoutParams android.widget.LinearLayout.LayoutParams}
* for layout attributes </p>
+ *
+ * @attr ref android.R.styleable#LinearLayout_baselineAligned
+ * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex
+ * @attr ref android.R.styleable#LinearLayout_gravity
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ * @attr ref android.R.styleable#LinearLayout_orientation
+ * @attr ref android.R.styleable#LinearLayout_weightSum
*/
@RemoteView
public class LinearLayout extends ViewGroup {
@@ -112,7 +119,11 @@ public class LinearLayout extends ViewGroup {
}
public LinearLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
+ this(context, attrs, 0);
+ }
+
+ public LinearLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
TypedArray a =
context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout);
@@ -137,8 +148,7 @@ public class LinearLayout extends ViewGroup {
mBaselineAlignedChildIndex =
a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1);
- // TODO: Better name, add Java APIs, make it public
- mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_useLargestChild, false);
+ mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_measureWithLargestChild, false);
a.recycle();
}
@@ -167,6 +177,33 @@ public class LinearLayout extends ViewGroup {
mBaselineAligned = baselineAligned;
}
+ /**
+ * When true, all children with a weight will be considered having
+ * the minimum size of the largest child. If false, all children are
+ * measured normally.
+ *
+ * @return True to measure children with a weight using the minimum
+ * size of the largest child, false otherwise.
+ */
+ public boolean isMeasureWithLargestChildEnabled() {
+ return mUseLargestChild;
+ }
+
+ /**
+ * When set to true, all children with a weight will be considered having
+ * the minimum size of the largest child. If false, all children are
+ * measured normally.
+ *
+ * Disabled by default.
+ *
+ * @param enabled True to measure children with a weight using the
+ * minimum size of the largest child, false otherwise.
+ */
+ @android.view.RemotableViewMethod
+ public void setMeasureWithLargestChildEnabled(boolean enabled) {
+ mUseLargestChild = enabled;
+ }
+
@Override
public int getBaseline() {
if (mBaselineAlignedChildIndex < 0) {
diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java
new file mode 100644
index 0000000..5c34c2c
--- /dev/null
+++ b/core/java/android/widget/ListPopupWindow.java
@@ -0,0 +1,1228 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+
+/**
+ * A ListPopupWindow anchors itself to a host view and displays a
+ * list of choices. When one is selected, the popup is dismissed.
+ *
+ * <p>ListPopupWindow contains a number of tricky behaviors surrounding
+ * positioning, scrolling parents to fit the dropdown, interacting
+ * sanely with the IME if present, and others.
+ *
+ * @see android.widget.AutoCompleteTextView
+ * @see android.widget.Spinner
+ */
+public class ListPopupWindow {
+ private static final String TAG = "ListPopupWindow";
+ private static final boolean DEBUG = false;
+
+ /**
+ * This value controls the length of time that the user
+ * must leave a pointer down without scrolling to expand
+ * the autocomplete dropdown list to cover the IME.
+ */
+ private static final int EXPAND_LIST_TIMEOUT = 250;
+
+ private Context mContext;
+ private PopupWindow mPopup;
+ private ListAdapter mAdapter;
+ private DropDownListView mDropDownList;
+
+ private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mDropDownHorizontalOffset;
+ private int mDropDownVerticalOffset;
+
+ private boolean mDropDownAlwaysVisible = false;
+ private boolean mForceIgnoreOutsideTouch = false;
+
+ private View mPromptView;
+ private int mPromptPosition = POSITION_PROMPT_ABOVE;
+
+ private DataSetObserver mObserver;
+
+ private View mDropDownAnchorView;
+
+ private Drawable mDropDownListHighlight;
+
+ private AdapterView.OnItemClickListener mItemClickListener;
+ private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+ private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
+ private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
+ private final PopupScrollListener mScrollListener = new PopupScrollListener();
+ private final ListSelectorHider mHideSelector = new ListSelectorHider();
+ private Runnable mShowDropDownRunnable;
+
+ private Handler mHandler = new Handler();
+
+ private Rect mTempRect = new Rect();
+
+ private boolean mModal;
+
+ /**
+ * The provided prompt view should appear above list content.
+ *
+ * @see #setPromptPosition(int)
+ * @see #getPromptPosition()
+ * @see #setPromptView(View)
+ */
+ public static final int POSITION_PROMPT_ABOVE = 0;
+
+ /**
+ * The provided prompt view should appear below list content.
+ *
+ * @see #setPromptPosition(int)
+ * @see #getPromptPosition()
+ * @see #setPromptView(View)
+ */
+ public static final int POSITION_PROMPT_BELOW = 1;
+
+ /**
+ * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
+ * If used to specify a popup width, the popup will match the width of the anchor view.
+ * If used to specify a popup height, the popup will fill available space.
+ */
+ public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
+
+ /**
+ * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
+ * If used to specify a popup width, the popup will use the width of its content.
+ */
+ public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ /**
+ * 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.
+ */
+ public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
+
+ /**
+ * 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 = PopupWindow.INPUT_METHOD_NEEDED;
+
+ /**
+ * 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.
+ */
+ public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ */
+ public ListPopupWindow(Context context) {
+ this(context, null, 0, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ */
+ public ListPopupWindow(Context context, AttributeSet attrs) {
+ this(context, attrs, 0, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ * @param defStyleAttr Default style attribute to use for popup content.
+ */
+ public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ * @param defStyleAttr Style attribute to read for default styling of popup content.
+ * @param defStyleRes Style resource ID to use for default styling of popup content.
+ */
+ public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ mContext = context;
+ mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Sets the adapter that provides the data and the views to represent the data
+ * in this popup window.
+ *
+ * @param adapter The adapter to use to create this window's content.
+ */
+ public void setAdapter(ListAdapter adapter) {
+ if (mObserver == null) {
+ mObserver = new PopupDataSetObserver();
+ } else if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ mAdapter = adapter;
+ if (mAdapter != null) {
+ adapter.registerDataSetObserver(mObserver);
+ }
+
+ if (mDropDownList != null) {
+ mDropDownList.setAdapter(mAdapter);
+ }
+ }
+
+ /**
+ * Set where the optional prompt view should appear. The default is
+ * {@link #POSITION_PROMPT_ABOVE}.
+ *
+ * @param position A position constant declaring where the prompt should be displayed.
+ *
+ * @see #POSITION_PROMPT_ABOVE
+ * @see #POSITION_PROMPT_BELOW
+ */
+ public void setPromptPosition(int position) {
+ mPromptPosition = position;
+ }
+
+ /**
+ * @return Where the optional prompt view should appear.
+ *
+ * @see #POSITION_PROMPT_ABOVE
+ * @see #POSITION_PROMPT_BELOW
+ */
+ public int getPromptPosition() {
+ return mPromptPosition;
+ }
+
+ /**
+ * Set whether this window should be modal when shown.
+ *
+ * <p>If a popup window is modal, it will receive all touch and key input.
+ * If the user touches outside the popup window's content area the popup window
+ * will be dismissed.
+ *
+ * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
+ */
+ public void setModal(boolean modal) {
+ mModal = true;
+ mPopup.setFocusable(modal);
+ }
+
+ /**
+ * Returns whether the popup window will be modal when shown.
+ *
+ * @return {@code true} if the popup window will be modal, {@code false} otherwise.
+ */
+ public boolean isModal() {
+ return mModal;
+ }
+
+ /**
+ * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
+ * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
+ * ignore outside touch even when the drop down is not set to always visible.
+ *
+ * @hide Used only by AutoCompleteTextView to handle some internal special cases.
+ */
+ public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
+ mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
+ }
+
+ /**
+ * Sets whether the drop-down should remain visible under certain conditions.
+ *
+ * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
+ * of the size or content of the list. {@link #getBackground()} will fill any space
+ * that is not used by the list.
+ *
+ * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+ *
+ * @hide Only used by AutoCompleteTextView under special conditions.
+ */
+ public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+ mDropDownAlwaysVisible = dropDownAlwaysVisible;
+ }
+
+ /**
+ * @return Whether the drop-down is visible under special conditions.
+ *
+ * @hide Only used by AutoCompleteTextView under special conditions.
+ */
+ public boolean isDropDownAlwaysVisible() {
+ return mDropDownAlwaysVisible;
+ }
+
+ /**
+ * Sets the operating mode for the soft input area.
+ *
+ * @param mode The desired mode, see
+ * {@link android.view.WindowManager.LayoutParams#softInputMode}
+ * for the full list
+ *
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ * @see #getSoftInputMode()
+ */
+ public void setSoftInputMode(int mode) {
+ mPopup.setSoftInputMode(mode);
+ }
+
+ /**
+ * Returns the current value in {@link #setSoftInputMode(int)}.
+ *
+ * @see #setSoftInputMode(int)
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ */
+ public int getSoftInputMode() {
+ return mPopup.getSoftInputMode();
+ }
+
+ /**
+ * Sets a drawable to use as the list item selector.
+ *
+ * @param selector List selector drawable to use in the popup.
+ */
+ public void setListSelector(Drawable selector) {
+ mDropDownListHighlight = selector;
+ }
+
+ /**
+ * @return The background drawable for the popup window.
+ */
+ public Drawable getBackground() {
+ return mPopup.getBackground();
+ }
+
+ /**
+ * Sets a drawable to be the background for the popup window.
+ *
+ * @param d A drawable to set as the background.
+ */
+ public void setBackgroundDrawable(Drawable d) {
+ mPopup.setBackgroundDrawable(d);
+ }
+
+ /**
+ * Set an animation style to use when the popup window is shown or dismissed.
+ *
+ * @param animationStyle Animation style to use.
+ */
+ public void setAnimationStyle(int animationStyle) {
+ mPopup.setAnimationStyle(animationStyle);
+ }
+
+ /**
+ * Returns the animation style that will be used when the popup window is
+ * shown or dismissed.
+ *
+ * @return Animation style that will be used.
+ */
+ public int getAnimationStyle() {
+ return mPopup.getAnimationStyle();
+ }
+
+ /**
+ * Returns the view that will be used to anchor this popup.
+ *
+ * @return The popup's anchor view
+ */
+ public View getAnchorView() {
+ return mDropDownAnchorView;
+ }
+
+ /**
+ * Sets the popup's anchor view. This popup will always be positioned relative to
+ * the anchor view when shown.
+ *
+ * @param anchor The view to use as an anchor.
+ */
+ public void setAnchorView(View anchor) {
+ mDropDownAnchorView = anchor;
+ }
+
+ /**
+ * @return The horizontal offset of the popup from its anchor in pixels.
+ */
+ public int getHorizontalOffset() {
+ return mDropDownHorizontalOffset;
+ }
+
+ /**
+ * Set the horizontal offset of this popup from its anchor view in pixels.
+ *
+ * @param offset The horizontal offset of the popup from its anchor.
+ */
+ public void setHorizontalOffset(int offset) {
+ mDropDownHorizontalOffset = offset;
+ }
+
+ /**
+ * @return The vertical offset of the popup from its anchor in pixels.
+ */
+ public int getVerticalOffset() {
+ return mDropDownVerticalOffset;
+ }
+
+ /**
+ * Set the vertical offset of this popup from its anchor view in pixels.
+ *
+ * @param offset The vertical offset of the popup from its anchor.
+ */
+ public void setVerticalOffset(int offset) {
+ mDropDownVerticalOffset = offset;
+ }
+
+ /**
+ * @return The width of the popup window in pixels.
+ */
+ public int getWidth() {
+ return mDropDownWidth;
+ }
+
+ /**
+ * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
+ * or {@link #WRAP_CONTENT}.
+ *
+ * @param width Width of the popup window.
+ */
+ public void setWidth(int width) {
+ mDropDownWidth = width;
+ }
+
+ /**
+ * Sets the width of the popup window by the size of its content. The final width may be
+ * larger to accommodate styled window dressing.
+ *
+ * @param width Desired width of content in pixels.
+ */
+ public void setContentWidth(int width) {
+ Drawable popupBackground = mPopup.getBackground();
+ if (popupBackground != null) {
+ mDropDownWidth = popupBackground.getIntrinsicWidth() + width;
+ }
+ }
+
+ /**
+ * @return The height of the popup window in pixels.
+ */
+ public int getHeight() {
+ return mDropDownHeight;
+ }
+
+ /**
+ * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
+ *
+ * @param height Height of the popup window.
+ */
+ public void setHeight(int height) {
+ mDropDownHeight = height;
+ }
+
+ /**
+ * Sets a listener to receive events when a list item is clicked.
+ *
+ * @param clickListener Listener to register
+ *
+ * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
+ */
+ public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
+ mItemClickListener = clickListener;
+ }
+
+ /**
+ * Sets a listener to receive events when a list item is selected.
+ *
+ * @param selectedListener Listener to register.
+ *
+ * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
+ */
+ public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) {
+ mItemSelectedListener = selectedListener;
+ }
+
+ /**
+ * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
+ * is controlled by {@link #setPromptPosition(int)}.
+ *
+ * @param prompt View to use as an informational prompt.
+ */
+ public void setPromptView(View prompt) {
+ boolean showing = isShowing();
+ if (showing) {
+ removePromptView();
+ }
+ mPromptView = prompt;
+ if (showing) {
+ show();
+ }
+ }
+
+ /**
+ * Post a {@link #show()} call to the UI thread.
+ */
+ public void postShow() {
+ mHandler.post(mShowDropDownRunnable);
+ }
+
+ /**
+ * Show the popup list. If the list is already showing, this method
+ * will recalculate the popup's size and position.
+ */
+ public void show() {
+ int height = buildDropDown();
+
+ int widthSpec = 0;
+ int heightSpec = 0;
+
+ boolean noInputMethod = isInputMethodNotNeeded();
+
+ if (mPopup.isShowing()) {
+ if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+ // The call to PopupWindow's update method below can accept -1 for any
+ // value you do not want to update.
+ widthSpec = -1;
+ } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ widthSpec = getAnchorView().getWidth();
+ } else {
+ widthSpec = mDropDownWidth;
+ }
+
+ if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_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.MATCH_PARENT;
+ if (noInputMethod) {
+ mPopup.setWindowLayoutMode(
+ mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+ ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
+ } else {
+ mPopup.setWindowLayoutMode(
+ mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+ ViewGroup.LayoutParams.MATCH_PARENT : 0,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+ } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ heightSpec = height;
+ } else {
+ heightSpec = mDropDownHeight;
+ }
+
+ mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+
+ mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
+ mDropDownVerticalOffset, widthSpec, heightSpec);
+ } else {
+ if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+ widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+ } else {
+ if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mPopup.setWidth(getAnchorView().getWidth());
+ } else {
+ mPopup.setWidth(mDropDownWidth);
+ }
+ }
+
+ if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ heightSpec = ViewGroup.LayoutParams.MATCH_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
+ // only set this if the dropdown is not always visible
+ mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+ mPopup.setTouchInterceptor(mTouchInterceptor);
+ mPopup.showAsDropDown(getAnchorView(),
+ mDropDownHorizontalOffset, mDropDownVerticalOffset);
+ mDropDownList.setSelection(ListView.INVALID_POSITION);
+
+ if (!mModal || mDropDownList.isInTouchMode()) {
+ clearListSelection();
+ }
+ if (!mModal) {
+ mHandler.post(mHideSelector);
+ }
+ }
+ }
+
+ /**
+ * Dismiss the popup window.
+ */
+ public void dismiss() {
+ mPopup.dismiss();
+ removePromptView();
+ mPopup.setContentView(null);
+ mDropDownList = null;
+ }
+
+ private void removePromptView() {
+ if (mPromptView != null) {
+ final ViewParent parent = mPromptView.getParent();
+ if (parent instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) parent;
+ group.removeView(mPromptView);
+ }
+ }
+ }
+
+ /**
+ * Control how the popup operates with an input method: one of
+ * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
+ * or {@link #INPUT_METHOD_NOT_NEEDED}.
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to the {@link #show()}
+ * method.</p>
+ *
+ * @see #getInputMethodMode()
+ * @see #show()
+ */
+ public void setInputMethodMode(int mode) {
+ mPopup.setInputMethodMode(mode);
+ }
+
+ /**
+ * Return the current value in {@link #setInputMethodMode(int)}.
+ *
+ * @see #setInputMethodMode(int)
+ */
+ public int getInputMethodMode() {
+ return mPopup.getInputMethodMode();
+ }
+
+ /**
+ * Set the selected position of the list.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ *
+ * @param position List position to set as selected.
+ */
+ public void setSelection(int position) {
+ DropDownListView list = mDropDownList;
+ if (isShowing() && list != null) {
+ list.mListSelectionHidden = false;
+ list.setSelection(position);
+ if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
+ list.setItemChecked(position, true);
+ }
+ }
+ }
+
+ /**
+ * Clear any current list selection.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ */
+ public void clearListSelection() {
+ final DropDownListView list = mDropDownList;
+ if (list != null) {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ list.mListSelectionHidden = true;
+ list.hideSelector();
+ list.requestLayout();
+ }
+ }
+
+ /**
+ * @return {@code true} if the popup is currently showing, {@code false} otherwise.
+ */
+ public boolean isShowing() {
+ return mPopup.isShowing();
+ }
+
+ /**
+ * @return {@code true} if this popup is configured to assume the user does not need
+ * to interact with the IME while it is showing, {@code false} otherwise.
+ */
+ public boolean isInputMethodNotNeeded() {
+ return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
+ }
+
+ /**
+ * Perform an item click operation on the specified list adapter position.
+ *
+ * @param position Adapter position for performing the click
+ * @return true if the click action could be performed, false if not.
+ * (e.g. if the popup was not showing, this method would return false.)
+ */
+ public boolean performItemClick(int position) {
+ if (isShowing()) {
+ if (mItemClickListener != null) {
+ final DropDownListView list = mDropDownList;
+ final View child = list.getChildAt(position - list.getFirstVisiblePosition());
+ mItemClickListener.onItemClick(list, child, position, child.getId());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return The currently selected item or null if the popup is not showing.
+ */
+ public Object getSelectedItem() {
+ if (!isShowing()) {
+ return null;
+ }
+ return mDropDownList.getSelectedItem();
+ }
+
+ /**
+ * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
+ * if {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedItemPosition()
+ */
+ public int getSelectedItemPosition() {
+ if (!isShowing()) {
+ return ListView.INVALID_POSITION;
+ }
+ return mDropDownList.getSelectedItemPosition();
+ }
+
+ /**
+ * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
+ * if {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedItemId()
+ */
+ public long getSelectedItemId() {
+ if (!isShowing()) {
+ return ListView.INVALID_ROW_ID;
+ }
+ return mDropDownList.getSelectedItemId();
+ }
+
+ /**
+ * @return The View for the currently selected item or null if
+ * {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedView()
+ */
+ public View getSelectedView() {
+ if (!isShowing()) {
+ return null;
+ }
+ return mDropDownList.getSelectedView();
+ }
+
+ /**
+ * @return The {@link ListView} displayed within the popup window.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ */
+ public ListView getListView() {
+ return mDropDownList;
+ }
+
+ /**
+ * Filter key down events. By forwarding key up events to this function,
+ * views using non-modal ListPopupWindow can have it handle key selection of items.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyDown
+ * @param event event param passed to the host view's onKeyDown
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // when the drop down is shown, we drive it directly
+ if (isShowing()) {
+ // the key events are forwarded to the list in the drop down view
+ // note that ListView handles space but we don't want that to happen
+ // also if selection is not currently in the drop down, then don't
+ // let center or enter presses go there since that would cause it
+ // to select one of its items
+ if (keyCode != KeyEvent.KEYCODE_SPACE
+ && (mDropDownList.getSelectedItemPosition() >= 0
+ || (keyCode != KeyEvent.KEYCODE_ENTER
+ && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) {
+ int curIndex = mDropDownList.getSelectedItemPosition();
+ boolean consumed;
+
+ final boolean below = !mPopup.isAboveAnchor();
+
+ final ListAdapter adapter = mAdapter;
+
+ boolean allEnabled;
+ int firstItem = Integer.MAX_VALUE;
+ int lastItem = Integer.MIN_VALUE;
+
+ if (adapter != null) {
+ allEnabled = adapter.areAllItemsEnabled();
+ firstItem = allEnabled ? 0 :
+ mDropDownList.lookForSelectablePosition(0, true);
+ lastItem = allEnabled ? adapter.getCount() - 1 :
+ mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
+ }
+
+ if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
+ (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
+ // When the selection is at the top, we block the key
+ // event to prevent focus from moving.
+ clearListSelection();
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ show();
+ 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 (consumed) {
+ // If it handled the key event, then the user is
+ // navigating in the list, so we should put it in front.
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ // Here's a little trick we need to do to make sure that
+ // the list view is actually showing its focus indicator,
+ // by ensuring it has focus and getting its window out
+ // of touch mode.
+ mDropDownList.requestFocusFromTouch();
+ show();
+
+ switch (keyCode) {
+ // avoid passing the focus from the text view to the
+ // next component
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ return true;
+ }
+ } else {
+ if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ // when the selection is at the bottom, we block the
+ // event to avoid going to the next focusable widget
+ if (curIndex == lastItem) {
+ return true;
+ }
+ } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
+ curIndex == firstItem) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Filter key down events. By forwarding key up events to this function,
+ * views using non-modal ListPopupWindow can have it handle key selection of items.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyUp
+ * @param event event param passed to the host view's onKeyUp
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
+ boolean consumed = mDropDownList.onKeyUp(keyCode, event);
+ if (consumed) {
+ switch (keyCode) {
+ // if the list accepts the key events and the key event
+ // was a click, the text view gets the selected item
+ // from the drop down as its content
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ dismiss();
+ break;
+ }
+ }
+ return consumed;
+ }
+ return false;
+ }
+
+ /**
+ * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
+ * events to this function, views using ListPopupWindow can have it dismiss the popup
+ * when the back key is pressed.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyPreIme
+ * @param event event param passed to the host view's onKeyPreIme
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
+ // special case for the back key, we do not even try to send it
+ // to the drop down list but instead, consume it immediately
+ final View anchorView = mDropDownAnchorView;
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ anchorView.getKeyDispatcherState().startTracking(event, this);
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ anchorView.getKeyDispatcherState().handleUpEvent(event);
+ if (event.isTracking() && !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * <p>Builds the popup window's content and returns the height the popup
+ * should have. Returns -1 when the content already exists.</p>
+ *
+ * @return the content's height or -1 if content already exists
+ */
+ private int buildDropDown() {
+ ViewGroup dropDownView;
+ int otherHeights = 0;
+
+ if (mDropDownList == null) {
+ Context context = mContext;
+
+ /**
+ * 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 = getAnchorView();
+ if (view != null && view.getWindowToken() != null) {
+ show();
+ }
+ }
+ };
+
+ mDropDownList = new DropDownListView(context, !mModal);
+ if (mDropDownListHighlight != null) {
+ mDropDownList.setSelector(mDropDownListHighlight);
+ }
+ mDropDownList.setAdapter(mAdapter);
+ mDropDownList.setVerticalFadingEdgeEnabled(true);
+ mDropDownList.setOnItemClickListener(mItemClickListener);
+ 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) {
+ }
+ });
+ mDropDownList.setOnScrollListener(mScrollListener);
+
+ if (mItemSelectedListener != null) {
+ mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
+ }
+
+ dropDownView = mDropDownList;
+
+ View hintView = mPromptView;
+ if (hintView != null) {
+ // if an hint has been specified, we accomodate more space for it and
+ // add a text view in the drop down menu, at the bottom of the list
+ LinearLayout hintContainer = new LinearLayout(context);
+ hintContainer.setOrientation(LinearLayout.VERTICAL);
+
+ LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
+ );
+
+ switch (mPromptPosition) {
+ case POSITION_PROMPT_BELOW:
+ hintContainer.addView(dropDownView, hintParams);
+ hintContainer.addView(hintView);
+ break;
+
+ case POSITION_PROMPT_ABOVE:
+ hintContainer.addView(hintView);
+ hintContainer.addView(dropDownView, hintParams);
+ break;
+
+ default:
+ Log.e(TAG, "Invalid hint position " + mPromptPosition);
+ break;
+ }
+
+ // measure the hint's height to find how much more vertical space
+ // we need to add to the drop down's height
+ int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
+ int heightSpec = MeasureSpec.UNSPECIFIED;
+ hintView.measure(widthSpec, heightSpec);
+
+ hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
+ otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+
+ dropDownView = hintContainer;
+ }
+
+ mPopup.setContentView(dropDownView);
+ } else {
+ dropDownView = (ViewGroup) mPopup.getContentView();
+ final View view = mPromptView;
+ if (view != null) {
+ LinearLayout.LayoutParams hintParams =
+ (LinearLayout.LayoutParams) view.getLayoutParams();
+ otherHeights = view.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+ }
+ }
+
+ // Max height available on the screen for a popup.
+ boolean ignoreBottomDecorations =
+ mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
+ final int maxHeight = mPopup.getMaxAvailableHeight(
+ getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
+
+ // getMaxAvailableHeight() subtracts the padding, so we put it back,
+ // to get the available height for the whole window
+ int padding = 0;
+ Drawable background = mPopup.getBackground();
+ if (background != null) {
+ background.getPadding(mTempRect);
+ padding = mTempRect.top + mTempRect.bottom;
+ }
+
+ if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ return maxHeight + padding;
+ }
+
+ final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
+ 0, ListView.NO_POSITION, maxHeight - otherHeights, 2);
+ // add padding only if the list has items in it, that way we don't show
+ // the popup if it is not needed
+ if (listContent > 0) otherHeights += padding;
+
+ return listContent + otherHeights;
+ }
+
+ /**
+ * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
+ * make sure the list uses the appropriate drawables and states when
+ * displayed on screen within a drop down. The focus is never actually
+ * passed to the drop down in this mode; the list only looks focused.</p>
+ */
+ private static class DropDownListView extends ListView {
+ private static final String TAG = ListPopupWindow.TAG + ".DropDownListView";
+ /*
+ * 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 explicitly hide the list's
+ * selection and reset to false whenever 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;
+
+ /**
+ * True if this wrapper should fake focus.
+ */
+ private boolean mHijackFocus;
+
+ /**
+ * <p>Creates a new list view wrapper.</p>
+ *
+ * @param context this view's context
+ */
+ public DropDownListView(Context context, boolean hijackFocus) {
+ super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
+ mHijackFocus = hijackFocus;
+ }
+
+ /**
+ * <p>Avoids jarring scrolling effect by ensuring that list elements
+ * made of a text view fit on a single line.</p>
+ *
+ * @param position the item index in the list to get a view for
+ * @return the view for the specified item
+ */
+ @Override
+ View obtainView(int position, boolean[] isScrap) {
+ View view = super.obtainView(position, isScrap);
+
+ if (view instanceof TextView) {
+ ((TextView) view).setHorizontallyScrolling(true);
+ }
+
+ return view;
+ }
+
+ @Override
+ public boolean isInTouchMode() {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasWindowFocus() {
+ return mHijackFocus || super.hasWindowFocus();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean isFocused() {
+ return mHijackFocus || super.isFocused();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return mHijackFocus || super.hasFocus();
+ }
+ }
+
+ private class PopupDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ if (isShowing()) {
+ // Resize the popup to fit new content
+ show();
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ dismiss();
+ }
+ }
+
+ private class ListSelectorHider implements Runnable {
+ public void run() {
+ clearListSelection();
+ }
+ }
+
+ private class ResizePopupRunnable implements Runnable {
+ public void run() {
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ show();
+ }
+ }
+
+ private class PopupTouchInterceptor implements OnTouchListener {
+ public boolean onTouch(View v, MotionEvent event) {
+ final int action = event.getAction();
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ if (action == MotionEvent.ACTION_DOWN &&
+ mPopup != null && mPopup.isShowing() &&
+ (x >= 0 && x < getWidth() && y >= 0 && y < getHeight())) {
+ mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
+ } else if (action == MotionEvent.ACTION_UP) {
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ }
+ return false;
+ }
+ }
+
+ private class PopupScrollListener implements ListView.OnScrollListener {
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
+ !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ mResizePopupRunnable.run();
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 892c44a..86913ae 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -415,7 +415,7 @@ public class ListView extends AbsListView {
*/
@Override
public void setAdapter(ListAdapter adapter) {
- if (null != mAdapter) {
+ if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
@@ -2968,7 +2968,7 @@ public class ListView extends AbsListView {
// 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 = drawDividers && isOpaque() && !super.isOpaque();
+ final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
mDividerPaint = new Paint();
@@ -2978,7 +2978,7 @@ public class ListView extends AbsListView {
final int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY;
if (!mStackFromBottom) {
- int bottom = 0;
+ int bottom;
final int scrollY = mScrollY;
for (int i = 0; i < count; i++) {
@@ -2987,18 +2987,16 @@ public class ListView extends AbsListView {
View child = getChildAt(i);
bottom = child.getBottom();
// Don't draw dividers next to items that are not enabled
- if (drawDividers) {
- 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);
- }
+ 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);
}
}
}
@@ -3014,7 +3012,7 @@ public class ListView extends AbsListView {
View child = getChildAt(i);
top = child.getTop();
// Don't draw dividers next to items that are not enabled
- if (drawDividers && top > listTop) {
+ if (top > listTop) {
if ((areAllItemsSelectable ||
(adapter.isEnabled(first + i) && (i == count - 1 ||
adapter.isEnabled(first + i + 1))))) {
@@ -3034,7 +3032,7 @@ public class ListView extends AbsListView {
}
}
- if (count > 0 && scrollY > 0 && drawDividers) {
+ if (count > 0 && scrollY > 0) {
bounds.top = listBottom;
bounds.bottom = listBottom + dividerHeight;
drawDivider(canvas, bounds, -1);
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
index 0378328..d404ce7 100644
--- a/core/java/android/widget/PopupWindow.java
+++ b/core/java/android/widget/PopupWindow.java
@@ -16,27 +16,28 @@
package android.widget;
-import com.android.internal.R;
+import java.lang.ref.WeakReference;
import android.content.Context;
import android.content.res.TypedArray;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.Gravity;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.ViewTreeObserver.OnScrollChangedListener;
-import android.view.View.OnTouchListener;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.os.IBinder;
import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.View.OnTouchListener;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
-import java.lang.ref.WeakReference;
+import com.android.internal.R;
/**
* <p>A popup window that can be used to display an arbitrary view. The popup
@@ -157,12 +158,21 @@ public class PopupWindow {
* <p>The popup does provide a background.</p>
*/
public PopupWindow(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ /**
+ * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p>
+ *
+ * <p>The popup does not provide a background.</p>
+ */
+ public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mContext = context;
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
TypedArray a =
context.obtainStyledAttributes(
- attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0);
+ attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes);
mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground);
@@ -1315,6 +1325,7 @@ public class PopupWindow {
}
private class PopupViewContainer extends FrameLayout {
+ private static final String TAG = "PopupWindow.PopupViewContainer";
public PopupViewContainer(Context context) {
super(context);
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
index 8e9eb05..71f0c2f 100644
--- a/core/java/android/widget/ProgressBar.java
+++ b/core/java/android/widget/ProgressBar.java
@@ -715,8 +715,8 @@ public class ProgressBar extends View {
mAnimation.setDuration(mDuration);
mAnimation.setInterpolator(mInterpolator);
mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);
- postInvalidate();
}
+ postInvalidate();
}
/**
@@ -729,6 +729,7 @@ public class ProgressBar extends View {
((Animatable) mIndeterminateDrawable).stop();
mShouldStartAnimationDrawable = false;
}
+ postInvalidate();
}
/**
diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java
index 07c3e4b..50fbb6b 100644
--- a/core/java/android/widget/QuickContactBadge.java
+++ b/core/java/android/widget/QuickContactBadge.java
@@ -48,6 +48,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener {
private QueryHandler mQueryHandler;
private Drawable mBadgeBackground;
private Drawable mNoBadgeBackground;
+ private Drawable mDefaultAvatar;
protected String[] mExcludeMimes = null;
@@ -117,6 +118,16 @@ public class QuickContactBadge extends ImageView implements OnClickListener {
public void setMode(int size) {
mMode = size;
}
+
+ /**
+ * Resets the contact photo to the default state.
+ */
+ public void setImageToDefault() {
+ if (mDefaultAvatar == null) {
+ mDefaultAvatar = getResources().getDrawable(R.drawable.ic_contact_picture);
+ }
+ setImageDrawable(mDefaultAvatar);
+ }
/**
* Assign the contact uri that this QuickContactBadge should be associated
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 7a70c80..fc02acf 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -60,12 +60,12 @@ public class RemoteViews implements Parcelable, Filter {
* The package name of the package containing the layout
* resource. (Added to the parcel)
*/
- private String mPackage;
+ private final String mPackage;
/**
* The resource ID of the layout file. (Added to the parcel)
*/
- private int mLayoutId;
+ private final int mLayoutId;
/**
* An array of actions to perform on the view tree once it has been
@@ -569,6 +569,7 @@ public class RemoteViews implements Parcelable, Filter {
}
}
+ @Override
public RemoteViews clone() {
final RemoteViews that = new RemoteViews(mPackage, mLayoutId);
if (mActions != null) {
diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java
index 7d3459e..d1c2270 100644
--- a/core/java/android/widget/SimpleCursorAdapter.java
+++ b/core/java/android/widget/SimpleCursorAdapter.java
@@ -62,7 +62,8 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter {
private int mStringConversionColumn = -1;
private CursorToStringConverter mCursorToStringConverter;
private ViewBinder mViewBinder;
- private String[] mOriginalFrom;
+
+ String[] mOriginalFrom;
/**
* Constructor.
diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java
index 2f6dd1e..60e8568 100644
--- a/core/java/android/widget/Spinner.java
+++ b/core/java/android/widget/Spinner.java
@@ -24,6 +24,7 @@ import android.content.DialogInterface.OnClickListener;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.util.AttributeSet;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -37,10 +38,21 @@ import android.view.ViewGroup;
*/
@Widget
public class Spinner extends AbsSpinner implements OnClickListener {
+ private static final String TAG = "Spinner";
+
+ /**
+ * Use a dialog window for selecting spinner options.
+ */
+ public static final int MODE_DIALOG = 0;
+
+ /**
+ * Use a dropdown anchored to the Spinner for selecting spinner options.
+ */
+ public static final int MODE_DROPDOWN = 1;
+
+ private SpinnerPopup mPopup;
+ private DropDownAdapter mTempAdapter;
- private CharSequence mPrompt;
- private AlertDialog mPopup;
-
public Spinner(Context context) {
this(context, null);
}
@@ -55,9 +67,54 @@ public class Spinner extends AbsSpinner implements OnClickListener {
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.Spinner, defStyle, 0);
- mPrompt = a.getString(com.android.internal.R.styleable.Spinner_prompt);
+ final int mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode,
+ MODE_DIALOG);
+
+ switch (mode) {
+ case MODE_DIALOG: {
+ mPopup = new DialogPopup();
+ break;
+ }
+
+ case MODE_DROPDOWN: {
+ final int hintResource = a.getResourceId(
+ com.android.internal.R.styleable.Spinner_popupPromptView, 0);
+
+ DropdownPopup popup = new DropdownPopup(context, attrs, defStyle, hintResource);
+
+ popup.setBackgroundDrawable(a.getDrawable(
+ com.android.internal.R.styleable.Spinner_popupBackground));
+ popup.setVerticalOffset(a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0));
+ popup.setHorizontalOffset(a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0));
+
+ mPopup = popup;
+ break;
+ }
+ }
+
+ mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt));
a.recycle();
+
+ // Base constructor can call setAdapter before we initialize mPopup.
+ // Finish setting things up if this happened.
+ if (mTempAdapter != null) {
+ mPopup.setAdapter(mTempAdapter);
+ mTempAdapter = null;
+ }
+ }
+
+ @Override
+ public void setAdapter(SpinnerAdapter adapter) {
+ super.setAdapter(adapter);
+
+ if (mPopup != null) {
+ mPopup.setAdapter(new DropDownAdapter(adapter));
+ } else {
+ mTempAdapter = new DropDownAdapter(adapter);
+ }
}
@Override
@@ -194,8 +251,6 @@ public class Spinner extends AbsSpinner implements OnClickListener {
return child;
}
-
-
/**
* Helper for makeAndAddView to set the position of a view
* and fill out its layout paramters.
@@ -246,15 +301,10 @@ public class Spinner extends AbsSpinner implements OnClickListener {
if (!handled) {
handled = true;
- Context context = getContext();
-
- final DropDownAdapter adapter = new DropDownAdapter(getAdapter());
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- if (mPrompt != null) {
- builder.setTitle(mPrompt);
+ if (!mPopup.isShowing()) {
+ mPopup.show();
}
- mPopup = builder.setSingleChoiceItems(adapter, getSelectedItemPosition(), this).show();
}
return handled;
@@ -271,7 +321,7 @@ public class Spinner extends AbsSpinner implements OnClickListener {
* @param prompt the prompt to set
*/
public void setPrompt(CharSequence prompt) {
- mPrompt = prompt;
+ mPopup.setPromptText(prompt);
}
/**
@@ -279,14 +329,14 @@ public class Spinner extends AbsSpinner implements OnClickListener {
* @param promptId the resource ID of the prompt to display when the dialog is shown
*/
public void setPromptId(int promptId) {
- mPrompt = getContext().getText(promptId);
+ setPrompt(getContext().getText(promptId));
}
/**
* @return The prompt to display when the dialog is shown
*/
public CharSequence getPrompt() {
- return mPrompt;
+ return mPopup.getHintText();
}
/**
@@ -384,4 +434,123 @@ public class Spinner extends AbsSpinner implements OnClickListener {
return getCount() == 0;
}
}
+
+ /**
+ * Implements some sort of popup selection interface for selecting a spinner option.
+ * Allows for different spinner modes.
+ */
+ private interface SpinnerPopup {
+ public void setAdapter(ListAdapter adapter);
+
+ /**
+ * Show the popup
+ */
+ public void show();
+
+ /**
+ * Dismiss the popup
+ */
+ public void dismiss();
+
+ /**
+ * @return true if the popup is showing, false otherwise.
+ */
+ public boolean isShowing();
+
+ /**
+ * Set hint text to be displayed to the user. This should provide
+ * a description of the choice being made.
+ * @param hintText Hint text to set.
+ */
+ public void setPromptText(CharSequence hintText);
+ public CharSequence getHintText();
+ }
+
+ private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
+ private AlertDialog mPopup;
+ private ListAdapter mListAdapter;
+ private CharSequence mPrompt;
+
+ public void dismiss() {
+ mPopup.dismiss();
+ mPopup = null;
+ }
+
+ public boolean isShowing() {
+ return mPopup != null ? mPopup.isShowing() : false;
+ }
+
+ public void setAdapter(ListAdapter adapter) {
+ mListAdapter = adapter;
+ }
+
+ public void setPromptText(CharSequence hintText) {
+ mPrompt = hintText;
+ }
+
+ public CharSequence getHintText() {
+ return mPrompt;
+ }
+
+ public void show() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ if (mPrompt != null) {
+ builder.setTitle(mPrompt);
+ }
+ mPopup = builder.setSingleChoiceItems(mListAdapter,
+ getSelectedItemPosition(), this).show();
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ setSelection(which);
+ dismiss();
+ }
+ }
+
+ private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
+ private CharSequence mHintText;
+ private TextView mHintView;
+ private int mHintResource;
+
+ public DropdownPopup(Context context, AttributeSet attrs,
+ int defStyleRes, int hintResource) {
+ super(context, attrs, 0, defStyleRes);
+
+ mHintResource = hintResource;
+
+ setAnchorView(Spinner.this);
+ setModal(true);
+ setPromptPosition(POSITION_PROMPT_BELOW);
+ setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView parent, View v, int position, long id) {
+ Spinner.this.setSelection(position);
+ dismiss();
+ }
+ });
+ }
+
+ public CharSequence getHintText() {
+ return mHintText;
+ }
+
+ public void setPromptText(CharSequence hintText) {
+ mHintText = hintText;
+ if (mHintView != null) {
+ mHintView.setText(hintText);
+ }
+ }
+
+ public void show() {
+ if (mHintView == null) {
+ final TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(
+ mHintResource, null).findViewById(com.android.internal.R.id.text1);
+ textView.setText(mHintText);
+ setPromptView(textView);
+ mHintView = textView;
+ }
+ super.show();
+ getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ setSelection(Spinner.this.getSelectedItemPosition());
+ }
+ }
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index e6ed70a..0ce8164 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -61,6 +61,7 @@ import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.method.ArrowKeyMovementMethod;
import android.text.method.DateKeyListener;
import android.text.method.DateTimeKeyListener;
import android.text.method.DialerKeyListener;
@@ -89,10 +90,11 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.ViewDebug;
+import android.view.ViewGroup.LayoutParams;
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;
@@ -185,7 +187,7 @@ import java.util.ArrayList;
*/
@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
- static final String TAG = "TextView";
+ static final String LOG_TAG = "TextView";
static final boolean DEBUG_EXTRACT = false;
private static int PRIORITY = 100;
@@ -321,6 +323,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
+ @SuppressWarnings("deprecation")
public TextView(Context context,
AttributeSet attrs,
int defStyle) {
@@ -695,9 +698,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
try {
setInputExtras(a.getResourceId(attr, 0));
} catch (XmlPullParserException e) {
- Log.w("TextView", "Failure reading input extras", e);
+ Log.w(LOG_TAG, "Failure reading input extras", e);
} catch (IOException e) {
- Log.w("TextView", "Failure reading input extras", e);
+ Log.w(LOG_TAG, "Failure reading input extras", e);
}
break;
}
@@ -714,7 +717,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
if (inputMethod != null) {
- Class c;
+ Class<?> c;
try {
c = Class.forName(inputMethod.toString());
@@ -923,6 +926,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setFocusable(focusable);
setClickable(clickable);
setLongClickable(longClickable);
+
+ prepareCursorController();
}
private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
@@ -1128,6 +1133,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setText(mText);
fixFocusableAndClickableSettings();
+ prepareCursorController();
}
private void fixFocusableAndClickableSettings() {
@@ -2335,6 +2341,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return str + "}";
}
+ @SuppressWarnings("hiding")
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
@@ -2369,8 +2376,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int end = 0;
if (mText != null) {
- start = Selection.getSelectionStart(mText);
- end = Selection.getSelectionEnd(mText);
+ start = getSelectionStart();
+ end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
save = true;
@@ -2442,7 +2449,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
restored = "(restored) ";
}
- Log.e("TextView", "Saved cursor position " + ss.selStart +
+ Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
"/" + ss.selEnd + " out of range for " + restored +
"text " + mText);
} else {
@@ -2694,6 +2701,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}
+
+ // Depends on canSelectText, which depends on text
+ prepareCursorController();
}
/**
@@ -2756,6 +2766,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return mChars[off + mStart];
}
+ @Override
public String toString() {
return new String(mChars, mStart, mLength);
}
@@ -2781,6 +2792,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
c.drawText(mChars, start + mStart, end - start, x, y, p);
}
+ public void drawTextRun(Canvas c, int start, int end,
+ int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
+ int count = end - start;
+ int contextCount = contextEnd - contextStart;
+ c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
+ contextCount, x, y, flags, p);
+ }
+
public float measureText(int start, int end, Paint p) {
return p.measureText(mChars, start + mStart, end - start);
}
@@ -2788,6 +2807,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public int getTextWidths(int start, int end, float[] widths, Paint p) {
return p.getTextWidths(mChars, start + mStart, end - start, widths);
}
+
+ public float getTextRunAdvances(int start, int end, int contextStart,
+ int contextEnd, int flags, float[] advances, int advancesIndex,
+ Paint p) {
+ int count = end - start;
+ int contextCount = contextEnd - contextStart;
+ return p.getTextRunAdvances(mChars, start + mStart, count,
+ contextStart + mStart, contextCount, flags, advances,
+ advancesIndex);
+ }
+
+ public int getTextRunCursor(int contextStart, int contextEnd, int flags,
+ int offset, int cursorOpt, Paint p) {
+ int contextCount = contextEnd - contextStart;
+ return p.getTextRunCursor(mChars, contextStart + mStart,
+ contextCount, flags, offset + mStart, cursorOpt);
+ }
}
/**
@@ -2981,7 +3017,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else {
input = TextKeyListener.getInstance();
}
- mInputType = type;
+ setRawInputType(type);
if (direct) mInput = input;
else {
setKeyListenerOnly(input);
@@ -3198,7 +3234,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*
* @param create If true, the extras will be created if they don't already
* exist. Otherwise, null will be returned if none have been created.
- * @see #setInputExtras(int)View
+ * @see #setInputExtras(int)
* @see EditorInfo#extras
* @attr ref android.R.styleable#TextView_editorExtras
*/
@@ -3312,7 +3348,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private static class ErrorPopup extends PopupWindow {
private boolean mAbove = false;
- private TextView mView;
+ private final TextView mView;
ErrorPopup(TextView v, int width, int height) {
super(v, width, height);
@@ -3585,7 +3621,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private void invalidateCursor() {
- int where = Selection.getSelectionEnd(mText);
+ int where = getSelectionEnd();
invalidateCursor(where, where, where);
}
@@ -3661,7 +3697,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
boolean changed = false;
if (mMovement != null) {
- int curs = Selection.getSelectionEnd(mText);
+ /* This code also provides auto-scrolling when a cursor is moved using a
+ * CursorController (insertion point or selection limits).
+ * For selection, ensure start or end is visible depending on controller's state.
+ */
+ int curs = getSelectionEnd();
+ if (mSelectionModifierCursorController != null) {
+ SelectionModifierCursorController selectionController =
+ (SelectionModifierCursorController) mSelectionModifierCursorController;
+ if (selectionController.isSelectionStartDragged()) {
+ curs = getSelectionStart();
+ }
+ }
/*
* TODO: This should really only keep the end in view if
@@ -3954,8 +4001,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// XXX This is not strictly true -- a program could set the
// selection manually if it really wanted to.
if (mMovement != null && (isFocused() || isPressed())) {
- selStart = Selection.getSelectionStart(mText);
- selEnd = Selection.getSelectionEnd(mText);
+ selStart = getSelectionStart();
+ selEnd = getSelectionEnd();
if (mCursorVisible && selStart >= 0 && isEnabled()) {
if (mHighlightPath == null)
@@ -4061,6 +4108,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
canvas.restore();
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.draw(canvas);
+ }
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.draw(canvas);
+ }
}
@Override
@@ -4475,8 +4529,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
outAttrs.hintText = mHint;
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
- outAttrs.initialSelStart = Selection.getSelectionStart(mText);
- outAttrs.initialSelEnd = Selection.getSelectionEnd(mText);
+ outAttrs.initialSelStart = getSelectionStart();
+ outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
return ic;
}
@@ -4561,8 +4615,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
}
outText.startOffset = 0;
- outText.selectionStart = Selection.getSelectionStart(content);
- outText.selectionEnd = Selection.getSelectionEnd(content);
+ outText.selectionStart = getSelectionStart();
+ outText.selectionEnd = getSelectionEnd();
return true;
}
return false;
@@ -4579,7 +4633,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (req != null) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
- if (DEBUG_EXTRACT) Log.v(TAG, "Retrieving extracted start="
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
+ ims.mChangedStart + " end=" + ims.mChangedEnd
+ " delta=" + ims.mChangedDelta);
if (ims.mChangedStart < 0 && !contentChanged) {
@@ -4587,7 +4641,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
ims.mChangedDelta, ims.mTmpExtracted)) {
- if (DEBUG_EXTRACT) Log.v(TAG, "Reporting extracted start="
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
+ ims.mTmpExtracted.partialStartOffset
+ " end=" + ims.mTmpExtracted.partialEndOffset
+ ": " + ims.mTmpExtracted.text);
@@ -4741,7 +4795,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
void updateAfterEdit() {
invalidate();
- int curs = Selection.getSelectionStart(mText);
+ int curs = getSelectionStart();
if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) ==
Gravity.BOTTOM) {
@@ -4756,7 +4810,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
makeBlink();
}
}
-
+
checkForResize();
}
@@ -4847,6 +4901,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
break;
case Gravity.RIGHT:
+ // Note, Layout resolves ALIGN_OPPOSITE to left or
+ // right based on the paragraph direction.
alignment = Layout.Alignment.ALIGN_OPPOSITE;
break;
@@ -4883,7 +4939,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
w, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}
- // Log.e("aaa", "Boring: " + mTransformed);
mSavedLayout = (BoringLayout) mLayout;
} else if (shouldEllipsize && boring.width <= w) {
@@ -5478,7 +5533,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// FIXME: Is it okay to truncate this, or should we round?
final int x = (int)mLayout.getPrimaryHorizontal(offset);
final int top = mLayout.getLineTop(line);
- final int bottom = mLayout.getLineTop(line+1);
+ final int bottom = mLayout.getLineTop(line + 1);
int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
@@ -5615,8 +5670,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// viewport coordinates, but requestRectangleOnScreen()
// is in terms of content coordinates.
- Rect r = new Rect();
- getInterestingRect(r, x, top, bottom, line);
+ Rect r = new Rect(x, top, x + 1, bottom);
+ getInterestingRect(r, line);
r.offset(mScrollX, mScrollY);
if (requestRectangleOnScreen(r)) {
@@ -5632,13 +5687,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* to the user. This will not move the cursor if it represents more than
* one character (a selection range). This will only work if the
* TextView contains spannable text; otherwise it will do nothing.
+ *
+ * @return True if the cursor was actually moved, false otherwise.
*/
public boolean moveCursorToVisibleOffset() {
if (!(mText instanceof Spannable)) {
return false;
}
- int start = Selection.getSelectionStart(mText);
- int end = Selection.getSelectionEnd(mText);
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
if (start != end) {
return false;
}
@@ -5648,7 +5705,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int line = mLayout.getLineForOffset(start);
final int top = mLayout.getLineTop(line);
- final int bottom = mLayout.getLineTop(line+1);
+ final int bottom = mLayout.getLineTop(line + 1);
final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
int vslack = (bottom - top) / 2;
if (vslack > vspace / 4)
@@ -5668,11 +5725,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
+ // line might contain bidirectional text
+ final int lowChar = leftChar < rightChar ? leftChar : rightChar;
+ final int highChar = leftChar > rightChar ? leftChar : rightChar;
+
int newStart = start;
- if (newStart < leftChar) {
- newStart = leftChar;
- } else if (newStart > rightChar) {
- newStart = rightChar;
+ if (newStart < lowChar) {
+ newStart = lowChar;
+ } else if (newStart > highChar) {
+ newStart = highChar;
}
if (newStart != start) {
@@ -5694,22 +5755,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- private void getInterestingRect(Rect r, int h, int top, int bottom,
- int line) {
+ private void getInterestingRect(Rect r, int line) {
+ convertFromViewportToContentCoordinates(r);
+
+ // Rectangle can can be expanded on first and last line to take
+ // padding into account.
+ // TODO Take left/right padding into account too?
+ if (line == 0) r.top -= getExtendedPaddingTop();
+ if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
+ }
+
+ private void convertFromViewportToContentCoordinates(Rect r) {
int paddingTop = getExtendedPaddingTop();
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
paddingTop += getVerticalOffset(false);
}
- top += paddingTop;
- bottom += paddingTop;
- h += getCompoundPaddingLeft();
+ r.top += paddingTop;
+ r.bottom += paddingTop;
- if (line == 0)
- top -= getExtendedPaddingTop();
- if (line == mLayout.getLineCount() - 1)
- bottom += getExtendedPaddingBottom();
+ int paddingLeft = getCompoundPaddingLeft();
+ r.left += paddingLeft;
+ r.right += paddingLeft;
- r.set(h, top, h+1, bottom);
r.offset(-mScrollX, -mScrollY);
}
@@ -5877,6 +5944,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else if (mBlink != null) {
mBlink.removeCallbacks(mBlink);
}
+ prepareCursorController();
}
private boolean canMarquee() {
@@ -5935,7 +6003,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private final WeakReference<TextView> mView;
private byte mStatus = MARQUEE_STOPPED;
- private float mScrollUnit;
+ private final float mScrollUnit;
private float mMaxScroll;
float mMaxFadeScroll;
private float mGhostStart;
@@ -5947,7 +6015,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
Marquee(TextView v) {
final float density = v.getContext().getResources().getDisplayMetrics().density;
- mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / (float) MARQUEE_RESOLUTION;
+ mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
mView = new WeakReference<TextView>(v);
}
@@ -6291,7 +6359,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
} else {
- if (DEBUG_EXTRACT) Log.v(TAG, "Span change outside of batch: "
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
+ oldStart + "-" + oldEnd + ","
+ newStart + "-" + newEnd + what);
ims.mContentChanged = true;
@@ -6307,7 +6375,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void beforeTextChanged(CharSequence buffer, int start,
int before, int after) {
- if (DEBUG_EXTRACT) Log.v(TAG, "beforeTextChanged start=" + start
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
+ " before=" + before + " after=" + after + ": " + buffer);
if (AccessibilityManager.getInstance(mContext).isEnabled()
@@ -6320,7 +6388,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onTextChanged(CharSequence buffer, int start,
int before, int after) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onTextChanged start=" + start
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
+ " before=" + before + " after=" + after + ": " + buffer);
TextView.this.handleTextChanged(buffer, start, before, after);
@@ -6330,10 +6398,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
mBeforeText = null;
}
+
+ // TODO. The cursor controller should hide as soon as text is typed.
+ // But this method is also used for cosmetic changes (underline current word when
+ // spell corrections are displayed. There is currently no way to make the difference
+ // between these cosmetic changes and actual text modifications.
}
public void afterTextChanged(Editable buffer) {
- if (DEBUG_EXTRACT) Log.v(TAG, "afterTextChanged: " + buffer);
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
TextView.this.sendAfterTextChanged(buffer);
if (MetaKeyKeyListener.getMetaState(buffer,
@@ -6344,19 +6417,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onSpanChanged(Spannable buf,
Object what, int s, int e, int st, int en) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onSpanChanged s=" + s + " e=" + e
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
+ " st=" + st + " en=" + en + " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, st, e, en);
}
public void onSpanAdded(Spannable buf, Object what, int s, int e) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onSpanAdded s=" + s + " e=" + e
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, -1, s, -1, e);
}
public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onSpanRemoved s=" + s + " e=" + e
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
@@ -6466,6 +6539,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
// Don't leave us in the middle of a batch edit.
onEndBatchEdit();
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
}
startStopMarquee(focused);
@@ -6532,24 +6612,39 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
class CommitSelectionReceiver extends ResultReceiver {
- int mNewStart;
- int mNewEnd;
-
- CommitSelectionReceiver() {
+ private final int mPrevStart, mPrevEnd;
+ private final int mNewStart, mNewEnd;
+
+ public CommitSelectionReceiver(int mPrevStart, int mPrevEnd, int mNewStart, int mNewEnd) {
super(getHandler());
+ this.mPrevStart = mPrevStart;
+ this.mPrevEnd = mPrevEnd;
+ this.mNewStart = mNewStart;
+ this.mNewEnd = mNewEnd;
}
-
+
+ @Override
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);
+ int start = mNewStart;
+ int end = mNewEnd;
+
+ // Move the cursor to the new position, unless this tap was actually
+ // use to show the IMM. Leave cursor unchanged in that case.
+ if (resultCode == InputMethodManager.RESULT_SHOWN) {
+ start = mPrevStart;
+ end = mPrevEnd;
+ } else if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.show();
+ }
+
+ final int len = mText.length();
+ if (start > len) {
+ start = len;
}
+ if (end > len) {
+ end = len;
+ }
+ Selection.setSelection((Spannable)mText, start, end);
}
}
@@ -6562,7 +6657,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mTouchFocusSelected = false;
mScrolled = false;
}
-
+
final boolean superResult = super.onTouchEvent(event);
/*
@@ -6575,44 +6670,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return superResult;
}
- if ((mMovement != null || onCheckIsTextEditor()) && mText instanceof Spannable && mLayout != null) {
+ if ((mMovement != null || onCheckIsTextEditor()) &&
+ mText instanceof Spannable && mLayout != null) {
boolean handled = false;
- int oldSelStart = Selection.getSelectionStart(mText);
- int oldSelEnd = Selection.getSelectionEnd(mText);
-
+ int oldSelStart = getSelectionStart();
+ int oldSelEnd = getSelectionEnd();
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onTouchEvent(event);
+ }
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onTouchEvent(event);
+ }
+
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
}
- if (mText instanceof Editable && onCheckIsTextEditor()) {
+ if (isTextEditable()) {
if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
-
- // This is going to be gross... if tapping on the text view
- // causes the IME to be displayed, we don't want the selection
- // to change. But the selection has already changed, and
- // we won't know right away whether the IME is getting
- // displayed, so...
-
- int newSelStart = Selection.getSelectionStart(mText);
- int newSelEnd = Selection.getSelectionEnd(mText);
+
+ final int newSelStart = getSelectionStart();
+ final int newSelEnd = getSelectionEnd();
+
CommitSelectionReceiver csr = null;
if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) {
- csr = new CommitSelectionReceiver();
- csr.mNewStart = newSelStart;
- csr.mNewEnd = newSelEnd;
- }
-
- if (imm.showSoftInput(this, 0, csr) && csr != null) {
- // The IME might get shown -- revert to the old
- // selection, and change to the new when we finally
- // find out of it is okay.
- Selection.setSelection((Spannable)mText, oldSelStart, oldSelEnd);
- handled = true;
+ csr = new CommitSelectionReceiver(oldSelStart, oldSelEnd,
+ newSelStart, newSelEnd);
}
+
+ handled |= imm.showSoftInput(this, 0, csr) && (csr != null);
}
}
@@ -6624,6 +6715,47 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return superResult;
}
+ private void prepareCursorController() {
+ boolean atLeastOneController = false;
+
+ // TODO Add an extra android:cursorController flag to disable the controller?
+ if (mCursorVisible) {
+ atLeastOneController = true;
+ if (mInsertionPointCursorController == null) {
+ mInsertionPointCursorController = new InsertionPointCursorController();
+ }
+ } else {
+ mInsertionPointCursorController = null;
+ }
+
+ if (canSelectText()) {
+ atLeastOneController = true;
+ if (mSelectionModifierCursorController == null) {
+ mSelectionModifierCursorController = new SelectionModifierCursorController();
+ }
+ } else {
+ mSelectionModifierCursorController = null;
+ }
+
+ if (atLeastOneController) {
+ if (sCursorControllerTempRect == null) {
+ sCursorControllerTempRect = new Rect();
+ }
+ Resources res = mContext.getResources();
+ mCursorControllerVerticalOffset = res.getDimensionPixelOffset(
+ com.android.internal.R.dimen.cursor_controller_vertical_offset);
+ } else {
+ sCursorControllerTempRect = null;
+ }
+ }
+
+ /**
+ * @return True iff this TextView contains a text that can be edited.
+ */
+ private boolean isTextEditable() {
+ return mText instanceof Editable && onCheckIsTextEditor();
+ }
+
/**
* Returns true, only while processing a touch gesture, if the initial
* touch down event caused focus to move to the text view and as a result
@@ -6657,7 +6789,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private static class Blink extends Handler implements Runnable {
- private WeakReference<TextView> mView;
+ private final WeakReference<TextView> mView;
private boolean mCancelled;
public Blink(TextView v) {
@@ -6674,8 +6806,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
TextView tv = mView.get();
if (tv != null && tv.isFocused()) {
- int st = Selection.getSelectionStart(tv.mText);
- int en = Selection.getSelectionEnd(tv.mText);
+ int st = tv.getSelectionStart();
+ int en = tv.getSelectionEnd();
if (st == en && st >= 0 && en >= 0) {
if (tv.mLayout != null) {
@@ -6752,8 +6884,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@Override
protected int computeHorizontalScrollRange() {
- if (mLayout != null)
- return mLayout.getWidth();
+ if (mLayout != null) {
+ return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
+ (int) mLayout.getLineWidth(0) : mLayout.getWidth();
+ }
return super.computeHorizontalScrollRange();
}
@@ -6865,6 +6999,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private boolean canSelectText() {
+ // prepareCursorController() relies on this method.
+ // If you change this condition, make sure prepareCursorController is called anywhere
+ // the value of this condition might be changed.
if (mText instanceof Spannable && mText.length() != 0 &&
mMovement != null && mMovement.canSelectArbitrarily()) {
return true;
@@ -6913,10 +7050,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
- * Returns a word to add to the dictionary from the context menu,
- * or null if there is no cursor or no word at the cursor.
+ * Returns the offsets delimiting the 'word' located at position offset.
+ *
+ * @param offset An offset in the text.
+ * @return The offsets for the start and end of the word located at <code>offset</code>.
+ * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits.
+ * Returns a negative value if no valid word was found.
*/
- private String getWordForDictionary() {
+ private long getWordLimitsAt(int offset) {
/*
* Quick return if the input type is one where adding words
* to the dictionary doesn't make any sense.
@@ -6925,7 +7066,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (klass == InputType.TYPE_CLASS_NUMBER ||
klass == InputType.TYPE_CLASS_PHONE ||
klass == InputType.TYPE_CLASS_DATETIME) {
- return null;
+ return -1;
}
int variation = mInputType & InputType.TYPE_MASK_VARIATION;
@@ -6934,13 +7075,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ||
variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
- return null;
+ return -1;
}
- int end = getSelectionEnd();
+ int end = offset;
if (end < 0) {
- return null;
+ return -1;
}
int start = end;
@@ -6974,6 +7115,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ if (start == end) {
+ return -1;
+ }
+
+ if (end - start > 48) {
+ return -1;
+ }
+
boolean hasLetter = false;
for (int i = start; i < end; i++) {
if (Character.isLetter(mTransformed.charAt(i))) {
@@ -6981,19 +7130,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
break;
}
}
+
if (!hasLetter) {
- return null;
+ return -1;
}
- if (start == end) {
- return null;
- }
+ // Two ints packed in a long
+ return (((long) start) << 32) | end;
+ }
- if (end - start > 48) {
+ /**
+ * Returns a word to add to the dictionary from the context menu,
+ * or null if there is no cursor or no word at the cursor.
+ */
+ private String getWordForDictionary() {
+ long wordLimits = getWordLimitsAt(getSelectionEnd());
+ if (wordLimits < 0) {
return null;
+ } else {
+ int start = (int) (wordLimits >>> 32);
+ int end = (int) (wordLimits & 0x00000000FFFFFFFFL);
+ return TextUtils.substring(mTransformed, start, end);
}
-
- return TextUtils.substring(mTransformed, start, end);
}
@Override
@@ -7291,7 +7449,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
+ @Override
public boolean performLongClick() {
+ // TODO This behavior should be moved to View
+ // TODO handle legacy code that added items to context menu
+ if (canSelectText()) {
+ if (startSelectionMode()) {
+ mEatTouchRelease = true;
+ return true;
+ }
+ }
+
if (super.performLongClick()) {
mEatTouchRelease = true;
return true;
@@ -7300,6 +7468,493 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
+ private boolean startSelectionMode() {
+ if (mSelectionModifierCursorController != null) {
+ int offset = ((SelectionModifierCursorController) mSelectionModifierCursorController).
+ getTouchOffset();
+
+ int selectionStart, selectionEnd;
+
+ if (hasSelection()) {
+ selectionStart = getSelectionStart();
+ selectionEnd = getSelectionEnd();
+ if (selectionStart > selectionEnd) {
+ int tmp = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tmp;
+ }
+ if ((offset >= selectionStart) && (offset <= selectionEnd)) {
+ // Long press in the current selection.
+ // Should initiate a drag. Return false, to rely on context menu for now.
+ return false;
+ }
+ }
+
+ long wordLimits = getWordLimitsAt(offset);
+ if (wordLimits >= 0) {
+ selectionStart = (int) (wordLimits >>> 32);
+ selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL);
+ } else {
+ selectionStart = Math.max(offset - 5, 0);
+ selectionEnd = Math.min(offset + 5, mText.length());
+ }
+
+ Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
+
+ // Has to be done AFTER selection has been changed to correctly position controllers.
+ mSelectionModifierCursorController.show();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the offset character closest to the specified absolute position.
+ *
+ * @param x The horizontal absolute position of a point on screen
+ * @param y The vertical absolute position of a point on screen
+ * @return the character offset for the character whose position is closest to the specified
+ * position.
+ *
+ * @hide
+ */
+ public int getOffset(int x, int y) {
+ x -= getTotalPaddingLeft();
+ y -= getTotalPaddingTop();
+
+ // Clamp the position to inside of the view.
+ if (x < 0) {
+ x = 0;
+ } else if (x >= (getWidth() - getTotalPaddingRight())) {
+ x = getWidth()-getTotalPaddingRight() - 1;
+ }
+ if (y < 0) {
+ y = 0;
+ } else if (y >= (getHeight() - getTotalPaddingBottom())) {
+ y = getHeight()-getTotalPaddingBottom() - 1;
+ }
+
+ x += getScrollX();
+ y += getScrollY();
+
+ Layout layout = getLayout();
+ final int line = layout.getLineForVertical(y);
+ final int offset = layout.getOffsetForHorizontal(line, x);
+ return offset;
+ }
+
+ /**
+ * A CursorController instance can be used to control a cursor in the text.
+ *
+ * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events
+ * and send them to this object instead of the cursor.
+ */
+ public interface CursorController {
+ /* Cursor fade-out animation duration, in milliseconds. */
+ static final int FADE_OUT_DURATION = 400;
+
+ /**
+ * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
+ * See also {@link #hide()}.
+ */
+ public void show();
+
+ /**
+ * Hide the cursor controller from screen.
+ * See also {@link #show()}.
+ */
+ public void hide();
+
+ /**
+ * Update the controller's position.
+ */
+ public void updatePosition(int offset);
+
+ /**
+ * The controller and the cursor's positions can be link by a fixed offset,
+ * computed when the controller is touched, and then maintained as it moves
+ * @return Horizontal offset between the controller and the cursor.
+ */
+ public float getOffsetX();
+
+ /**
+ * @return Vertical offset between the controller and the cursor.
+ */
+ public float getOffsetY();
+
+ /**
+ * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
+ * a chance to become active and/or visible.
+ * @param event The touch event
+ */
+ public void onTouchEvent(MotionEvent event);
+
+ /**
+ * Draws a visual representation of the controller on the canvas.
+ *
+ * Called at the end of {@link #draw(Canvas)}, in the content coordinates system.
+ * @param canvas The Canvas used by this TextView.
+ */
+ public void draw(Canvas canvas);
+ }
+
+ class InsertionPointCursorController implements CursorController {
+ private static final int DELAY_BEFORE_FADE_OUT = 2100;
+
+ // Whether or not the cursor control is currently visible
+ private boolean mIsVisible = false;
+ // Starting time of the fade timer
+ private long mFadeOutTimerStart;
+ // The cursor controller image
+ private final Drawable mDrawable;
+ // Used to detect a tap (vs drag) on the controller
+ private long mOnDownTimerStart;
+ // Offset between finger hot point on cursor controller and actual cursor
+ private float mOffsetX, mOffsetY;
+
+ InsertionPointCursorController() {
+ Resources res = mContext.getResources();
+ mDrawable = res.getDrawable(com.android.internal.R.drawable.cursor_controller);
+ }
+
+ public void show() {
+ updateDrawablePosition();
+ // Has to be done after updatePosition, so that previous position invalidate
+ // in only done if necessary.
+ mIsVisible = true;
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
+ }
+
+ public void hide() {
+ if (mIsVisible) {
+ long time = System.currentTimeMillis();
+ // Start fading out, only if not already in progress
+ if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) {
+ mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT;
+ postInvalidate(mDrawable);
+ }
+ }
+ }
+
+ public void draw(Canvas canvas) {
+ if (mIsVisible) {
+ int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart);
+ if (time <= DELAY_BEFORE_FADE_OUT) {
+ postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time, mDrawable);
+ } else {
+ time -= DELAY_BEFORE_FADE_OUT;
+ if (time <= FADE_OUT_DURATION) {
+ final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
+ mDrawable.setAlpha(alpha);
+ postInvalidateDelayed(30, mDrawable);
+ } else {
+ mDrawable.setAlpha(0);
+ mIsVisible = false;
+ }
+ }
+ mDrawable.draw(canvas);
+ }
+ }
+
+ public void updatePosition(int offset) {
+ Selection.setSelection((Spannable) mText, offset);
+ updateDrawablePosition();
+ }
+
+ private void updateDrawablePosition() {
+ if (mIsVisible) {
+ // Clear previous cursor controller before bounds are updated
+ postInvalidate(mDrawable);
+ }
+
+ final int offset = getSelectionStart();
+
+ if (offset < 0) {
+ // Should never happen, safety check.
+ Log.w(LOG_TAG, "Update cursor controller position called with no cursor");
+ mIsVisible = false;
+ return;
+ }
+
+ positionDrawableUnderCursor(offset, mDrawable);
+
+ mFadeOutTimerStart = System.currentTimeMillis();
+ mDrawable.setAlpha(255);
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ if (isFocused() && isTextEditable() && mIsVisible) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN : {
+ final float x = event.getX();
+ final float y = event.getY();
+
+ if (fingerIsOnDrawable(x, y, mDrawable)) {
+ show();
+
+ if (mMovement instanceof ArrowKeyMovementMethod) {
+ ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
+ }
+
+ if (mParent != null) {
+ // Prevent possible scrollView parent from scrolling, so that
+ // we can use auto-scrolling.
+ mParent.requestDisallowInterceptTouchEvent(true);
+
+ final Rect bounds = mDrawable.getBounds();
+ mOffsetX = (bounds.left + bounds.right) / 2.0f - x;
+ mOffsetY = bounds.top - mCursorControllerVerticalOffset - y;
+
+ mOnDownTimerStart = event.getEventTime();
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_UP : {
+ int time = (int) (event.getEventTime() - mOnDownTimerStart);
+
+ if (time <= ViewConfiguration.getTapTimeout()) {
+ // A tap on the controller is not grabbed, move the cursor instead
+ int offset = getOffset((int) event.getX(), (int) event.getY());
+ Selection.setSelection((Spannable) mText, offset);
+
+ // Modified by cancelLongPress and prevents the cursor from changing
+ mScrolled = false;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public float getOffsetX() {
+ return mOffsetX;
+ }
+
+ public float getOffsetY() {
+ return mOffsetY;
+ }
+ }
+
+ class SelectionModifierCursorController implements CursorController {
+ // Whether or not the selection controls are currently visible
+ private boolean mIsVisible = false;
+ // Whether that start or the end of selection controller is dragged
+ private boolean mStartIsDragged = false;
+ // Starting time of the fade timer
+ private long mFadeOutTimerStart;
+ // The cursor controller images
+ private final Drawable mStartDrawable, mEndDrawable;
+ // Offset between finger hot point on active cursor controller and actual cursor
+ private float mOffsetX, mOffsetY;
+ // The offset of that last touch down event. Remembered to start selection there.
+ private int mTouchOffset;
+
+ SelectionModifierCursorController() {
+ Resources res = mContext.getResources();
+ mStartDrawable = res.getDrawable(com.android.internal.R.drawable.selection_start_handle);
+ mEndDrawable = res.getDrawable(com.android.internal.R.drawable.selection_end_handle);
+ }
+
+ public void show() {
+ updateDrawablesPositions();
+ // Has to be done after updatePosition, so that previous position invalidate
+ // in only done if necessary.
+ mIsVisible = true;
+ mFadeOutTimerStart = -1;
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ }
+
+ public void hide() {
+ if (mIsVisible && (mFadeOutTimerStart < 0)) {
+ mFadeOutTimerStart = System.currentTimeMillis();
+ postInvalidate(mStartDrawable);
+ postInvalidate(mEndDrawable);
+ }
+ }
+
+ public void draw(Canvas canvas) {
+ if (mIsVisible) {
+ if (mFadeOutTimerStart >= 0) {
+ int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart);
+ if (time <= FADE_OUT_DURATION) {
+ final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
+ mStartDrawable.setAlpha(alpha);
+ mEndDrawable.setAlpha(alpha);
+ postInvalidateDelayed(30, mStartDrawable);
+ postInvalidateDelayed(30, mEndDrawable);
+ } else {
+ mStartDrawable.setAlpha(0);
+ mEndDrawable.setAlpha(0);
+ mIsVisible = false;
+ }
+ }
+ mStartDrawable.draw(canvas);
+ mEndDrawable.draw(canvas);
+ }
+ }
+
+ public void updatePosition(int offset) {
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
+
+ // Handle the case where start and end are swapped, making sure start <= end
+ if (mStartIsDragged) {
+ if (offset <= selectionEnd) {
+ selectionStart = offset;
+ } else {
+ selectionStart = selectionEnd;
+ selectionEnd = offset;
+ mStartIsDragged = false;
+ }
+ } else {
+ if (offset >= selectionStart) {
+ selectionEnd = offset;
+ } else {
+ selectionEnd = selectionStart;
+ selectionStart = offset;
+ mStartIsDragged = true;
+ }
+ }
+
+ Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
+ updateDrawablesPositions();
+ }
+
+ private void updateDrawablesPositions() {
+ if (mIsVisible) {
+ // Clear previous cursor controller before bounds are updated
+ postInvalidate(mStartDrawable);
+ postInvalidate(mEndDrawable);
+ }
+
+ final int selectionStart = getSelectionStart();
+ final int selectionEnd = getSelectionEnd();
+
+ if ((selectionStart < 0) || (selectionEnd < 0)) {
+ // Should never happen, safety check.
+ Log.w(LOG_TAG, "Update selection controller position called with no cursor");
+ mIsVisible = false;
+ return;
+ }
+
+ positionDrawableUnderCursor(selectionStart, mStartDrawable);
+ positionDrawableUnderCursor(selectionEnd, mEndDrawable);
+
+ mStartDrawable.setAlpha(255);
+ mEndDrawable.setAlpha(255);
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ if (isFocused() && isTextEditable() &&
+ (event.getActionMasked() == MotionEvent.ACTION_DOWN)) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ // Remember finger down position, to be able to start selection on that point
+ mTouchOffset = getOffset(x, y);
+
+ if (mIsVisible) {
+ if (mMovement instanceof ArrowKeyMovementMethod) {
+ boolean isOnStart = fingerIsOnDrawable(x, y, mStartDrawable);
+ boolean isOnEnd = fingerIsOnDrawable(x, y, mEndDrawable);
+ if (isOnStart || isOnEnd) {
+ if (mParent != null) {
+ // Prevent possible scrollView parent from scrolling, so that
+ // we can use auto-scrolling.
+ mParent.requestDisallowInterceptTouchEvent(true);
+ }
+
+ // Start handle will be dragged in case BOTH controller are under finger
+ mStartIsDragged = isOnStart;
+ final Rect bounds =
+ (mStartIsDragged ? mStartDrawable : mEndDrawable).getBounds();
+ mOffsetX = (bounds.left + bounds.right) / 2.0f - x;
+ mOffsetY = bounds.top - mCursorControllerVerticalOffset - y;
+
+ ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
+ }
+ }
+ }
+ }
+ }
+
+ public int getTouchOffset() {
+ return mTouchOffset;
+ }
+
+ public float getOffsetX() {
+ return mOffsetX;
+ }
+
+ public float getOffsetY() {
+ return mOffsetY;
+ }
+
+ /**
+ * @return true iff this controller is currently used to move the selection start.
+ */
+ public boolean isSelectionStartDragged() {
+ return mIsVisible && mStartIsDragged;
+ }
+ }
+
+ // Helper methods used by CursorController implementations
+
+ private void positionDrawableUnderCursor(final int offset, Drawable drawable) {
+ final int drawableWidth = drawable.getIntrinsicWidth();
+ final int drawableHeight = drawable.getIntrinsicHeight();
+ final int line = mLayout.getLineForOffset(offset);
+
+ final Rect bounds = sCursorControllerTempRect;
+ bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5 - drawableWidth / 2.0);
+ bounds.top = mLayout.getLineTop(line + 1);
+
+ // Move cursor controller a little bit up when editing the last line of text
+ // (or a single line) so that it is visible and easier to grab.
+ if (line == mLayout.getLineCount() - 1) {
+ bounds.top -= Math.max(0, drawableHeight / 2 - getExtendedPaddingBottom());
+ }
+
+ bounds.right = bounds.left + drawableWidth;
+ bounds.bottom = bounds.top + drawableHeight;
+
+ convertFromViewportToContentCoordinates(bounds);
+ drawable.setBounds(bounds);
+ postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ private boolean fingerIsOnDrawable(float x, float y, Drawable drawable) {
+ // Simulate a 'fat finger' to ease grabbing of the controller.
+ // Expands according to controller image size instead of using density.
+ // Assumes controller imager has a sensible size, proportionnal to density.
+ final int drawableWidth = drawable.getIntrinsicWidth();
+ final int drawableHeight = drawable.getIntrinsicHeight();
+ final Rect fingerRect = sCursorControllerTempRect;
+ fingerRect.set((int) (x - drawableWidth / 2.0),
+ (int) (y - drawableHeight),
+ (int) (x + drawableWidth / 2.0),
+ (int) y);
+ return Rect.intersects(drawable.getBounds(), fingerRect);
+ }
+
+ private void postInvalidate(Drawable drawable) {
+ final Rect bounds = drawable.getBounds();
+ postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ private void postInvalidateDelayed(long delay, Drawable drawable) {
+ final Rect bounds = drawable.getBounds();
+ postInvalidateDelayed(delay, bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
@ViewDebug.ExportedProperty
private CharSequence mText;
private CharSequence mTransformed;
@@ -7318,16 +7973,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private ArrayList<TextWatcher> mListeners = null;
// display attributes
- private TextPaint mTextPaint;
+ private final TextPaint mTextPaint;
private boolean mUserSetTextScaleX;
- private Paint mHighlightPaint;
- private int mHighlightColor = 0xFFBBDDFF;
+ private final Paint mHighlightPaint;
+ private int mHighlightColor = 0xD077A14B;
private Layout mLayout;
private long mShowCursor;
private Blink mBlink;
private boolean mCursorVisible = true;
+ // Cursor Controllers. Null when disabled.
+ private CursorController mInsertionPointCursorController;
+ private CursorController mSelectionModifierCursorController;
+ // Stored once and for all.
+ private int mCursorControllerVerticalOffset;
+ // Created once and shared by different CursorController helper methods.
+ private static Rect sCursorControllerTempRect;
+
private boolean mSelectAllOnFocus = false;
private int mGravity = Gravity.TOP | Gravity.LEFT;
diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java
index 907cfb3..7b66893 100644
--- a/core/java/android/widget/ViewAnimator.java
+++ b/core/java/android/widget/ViewAnimator.java
@@ -31,11 +31,13 @@ import android.view.animation.AnimationUtils;
*
* @attr ref android.R.styleable#ViewAnimator_inAnimation
* @attr ref android.R.styleable#ViewAnimator_outAnimation
+ * @attr ref android.R.styleable#ViewAnimator_animateFirstView
*/
public class ViewAnimator extends FrameLayout {
int mWhichChild = 0;
boolean mFirstTime = true;
+
boolean mAnimateFirstTime = true;
Animation mInAnimation;
@@ -59,6 +61,10 @@ public class ViewAnimator extends FrameLayout {
if (resource > 0) {
setOutAnimation(context, resource);
}
+
+ boolean flag = a.getBoolean(com.android.internal.R.styleable.ViewAnimator_animateFirstView, true);
+ setAnimateFirstView(flag);
+
a.recycle();
initViewAnimator(context, attrs);
@@ -84,10 +90,10 @@ public class ViewAnimator extends FrameLayout {
setMeasureAllChildren(measureAllChildren);
a.recycle();
}
-
+
/**
* Sets which child view will be displayed.
- *
+ *
* @param whichChild the index of the child view to display
*/
public void setDisplayedChild(int whichChild) {
@@ -105,14 +111,14 @@ public class ViewAnimator extends FrameLayout {
requestFocus(FOCUS_FORWARD);
}
}
-
+
/**
* Returns the index of the currently displayed child view.
*/
public int getDisplayedChild() {
return mWhichChild;
}
-
+
/**
* Manually shows the next child.
*/
@@ -128,25 +134,27 @@ public class ViewAnimator extends FrameLayout {
}
/**
- * Shows only the specified child. The other displays Views exit the screen
- * with the {@link #getOutAnimation() out animation} and the specified child
- * enters the screen with the {@link #getInAnimation() in animation}.
+ * Shows only the specified child. The other displays Views exit the screen,
+ * optionally with the with the {@link #getOutAnimation() out animation} and
+ * the specified child enters the screen, optionally with the
+ * {@link #getInAnimation() in animation}.
*
* @param childIndex The index of the child to be shown.
+ * @param animate Whether or not to use the in and out animations, defaults
+ * to true.
*/
- void showOnly(int childIndex) {
+ void showOnly(int childIndex, boolean animate) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
- final boolean checkForFirst = (!mFirstTime || mAnimateFirstTime);
if (i == childIndex) {
- if (checkForFirst && mInAnimation != null) {
+ if (animate && mInAnimation != null) {
child.startAnimation(mInAnimation);
}
child.setVisibility(View.VISIBLE);
mFirstTime = false;
} else {
- if (checkForFirst && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
+ if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
child.startAnimation(mOutAnimation);
} else if (child.getAnimation() == mInAnimation)
child.clearAnimation();
@@ -154,6 +162,17 @@ public class ViewAnimator extends FrameLayout {
}
}
}
+ /**
+ * Shows only the specified child. The other displays Views exit the screen
+ * with the {@link #getOutAnimation() out animation} and the specified child
+ * enters the screen with the {@link #getInAnimation() in animation}.
+ *
+ * @param childIndex The index of the child to be shown.
+ */
+ void showOnly(int childIndex) {
+ final boolean animate = (!mFirstTime || mAnimateFirstTime);
+ showOnly(childIndex, animate);
+ }
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java
index 8034961..c6f6e81 100644
--- a/core/java/android/widget/ViewFlipper.java
+++ b/core/java/android/widget/ViewFlipper.java
@@ -75,7 +75,7 @@ public class ViewFlipper extends ViewAnimator {
updateRunning();
} else if (Intent.ACTION_USER_PRESENT.equals(action)) {
mUserPresent = true;
- updateRunning();
+ updateRunning(false);
}
}
};
@@ -109,7 +109,7 @@ public class ViewFlipper extends ViewAnimator {
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
mVisible = visibility == VISIBLE;
- updateRunning();
+ updateRunning(false);
}
/**
@@ -144,10 +144,22 @@ public class ViewFlipper extends ViewAnimator {
* on {@link #mRunning} and {@link #mVisible} state.
*/
private void updateRunning() {
+ updateRunning(true);
+ }
+
+ /**
+ * Internal method to start or stop dispatching flip {@link Message} based
+ * on {@link #mRunning} and {@link #mVisible} state.
+ *
+ * @param flipNow Determines whether or not to execute the animation now, in
+ * addition to queuing future flips. If omitted, defaults to
+ * true.
+ */
+ private void updateRunning(boolean flipNow) {
boolean running = mVisible && mStarted && mUserPresent;
if (running != mRunning) {
if (running) {
- showOnly(mWhichChild);
+ showOnly(mWhichChild, flipNow);
Message msg = mHandler.obtainMessage(FLIP_MSG);
mHandler.sendMessageDelayed(msg, mFlipInterval);
} else {
diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java
index 3df419a..450c966 100644
--- a/core/java/android/widget/ZoomButtonsController.java
+++ b/core/java/android/widget/ZoomButtonsController.java
@@ -66,8 +66,9 @@ import android.view.WindowManager.LayoutParams;
* {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}.
* <p>
* If you are using this with a custom View, please call
- * {@link #setVisible(boolean) setVisible(false)} from the
- * {@link View#onDetachedFromWindow}.
+ * {@link #setVisible(boolean) setVisible(false)} from
+ * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged}
+ * when <code>visibility != View.VISIBLE</code>.
*
*/
public class ZoomButtonsController implements View.OnTouchListener {
diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java
new file mode 100644
index 0000000..f37021b
--- /dev/null
+++ b/core/java/com/android/internal/app/ActionBarImpl.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import com.android.internal.view.menu.ActionMenu;
+import com.android.internal.view.menu.ActionMenuItem;
+import com.android.internal.widget.ActionBarContextView;
+import com.android.internal.widget.ActionBarView;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.SpinnerAdapter;
+import android.widget.ViewAnimator;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * ActionBarImpl is the ActionBar implementation used
+ * by devices of all screen sizes. If it detects a compatible decor,
+ * it will split contextual modes across both the ActionBarView at
+ * the top of the screen and a horizontal LinearLayout at the bottom
+ * which is normally hidden.
+ */
+public class ActionBarImpl extends ActionBar {
+ private static final int NORMAL_VIEW = 0;
+ private static final int CONTEXT_VIEW = 1;
+
+ private static final int TAB_SWITCH_SHOW_HIDE = 0;
+ private static final int TAB_SWITCH_ADD_REMOVE = 1;
+
+ private Activity mActivity;
+
+ private ViewAnimator mAnimatorView;
+ private ActionBarView mActionView;
+ private ActionBarContextView mUpperContextView;
+ private LinearLayout mLowerContextView;
+
+ private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>();
+
+ private int mTabContainerViewId = android.R.id.content;
+ private TabImpl mSelectedTab;
+ private int mTabSwitchMode = TAB_SWITCH_ADD_REMOVE;
+
+ private ContextMode mContextMode;
+
+ private static final int CONTEXT_DISPLAY_NORMAL = 0;
+ private static final int CONTEXT_DISPLAY_SPLIT = 1;
+
+ private int mContextDisplayMode;
+
+ private boolean mClosingContext;
+
+ final Handler mHandler = new Handler();
+ final Runnable mCloseContext = new Runnable() {
+ public void run() {
+ mUpperContextView.closeMode();
+ if (mLowerContextView != null) {
+ mLowerContextView.removeAllViews();
+ }
+ mClosingContext = false;
+ }
+ };
+
+ public ActionBarImpl(Activity activity) {
+ final View decor = activity.getWindow().getDecorView();
+ mActivity = activity;
+ mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar);
+ mUpperContextView = (ActionBarContextView) decor.findViewById(
+ com.android.internal.R.id.action_context_bar);
+ mLowerContextView = (LinearLayout) decor.findViewById(
+ com.android.internal.R.id.lower_action_context_bar);
+ mAnimatorView = (ViewAnimator) decor.findViewById(
+ com.android.internal.R.id.action_bar_animator);
+
+ if (mActionView == null || mUpperContextView == null || mAnimatorView == null) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with a compatible window decor layout");
+ }
+
+ mContextDisplayMode = mLowerContextView == null ?
+ CONTEXT_DISPLAY_NORMAL : CONTEXT_DISPLAY_SPLIT;
+ }
+
+ public void setCustomNavigationMode(View view) {
+ cleanupTabs();
+ mActionView.setCustomNavigationView(view);
+ mActionView.setCallback(null);
+ }
+
+ public void setDropdownNavigationMode(SpinnerAdapter adapter, NavigationCallback callback) {
+ cleanupTabs();
+ mActionView.setCallback(callback);
+ mActionView.setNavigationMode(NAVIGATION_MODE_DROPDOWN_LIST);
+ mActionView.setDropdownAdapter(adapter);
+ }
+
+ public void setStandardNavigationMode() {
+ cleanupTabs();
+ mActionView.setNavigationMode(NAVIGATION_MODE_STANDARD);
+ mActionView.setCallback(null);
+ }
+
+ public void setStandardNavigationMode(CharSequence title) {
+ cleanupTabs();
+ setStandardNavigationMode(title, null);
+ }
+
+ public void setStandardNavigationMode(CharSequence title, CharSequence subtitle) {
+ cleanupTabs();
+ mActionView.setNavigationMode(NAVIGATION_MODE_STANDARD);
+ mActionView.setTitle(title);
+ mActionView.setSubtitle(subtitle);
+ mActionView.setCallback(null);
+ }
+
+ private void cleanupTabs() {
+ if (mSelectedTab != null) {
+ selectTab(null);
+ }
+ if (!mTabs.isEmpty()) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ final FragmentTransaction trans = mActivity.openFragmentTransaction();
+ final int tabCount = mTabs.size();
+ for (int i = 0; i < tabCount; i++) {
+ trans.remove(mTabs.get(i).getFragment());
+ }
+ trans.commit();
+ }
+ mTabs.clear();
+ }
+ }
+
+ public void setTitle(CharSequence title) {
+ mActionView.setTitle(title);
+ }
+
+ public void setSubtitle(CharSequence subtitle) {
+ mActionView.setSubtitle(subtitle);
+ }
+
+ public void setDisplayOptions(int options) {
+ mActionView.setDisplayOptions(options);
+ }
+
+ public void setDisplayOptions(int options, int mask) {
+ final int current = mActionView.getDisplayOptions();
+ mActionView.setDisplayOptions((options & mask) | (current & ~mask));
+ }
+
+ public void setBackgroundDrawable(Drawable d) {
+ mActionView.setBackgroundDrawable(d);
+ }
+
+ public View getCustomNavigationView() {
+ return mActionView.getCustomNavigationView();
+ }
+
+ public CharSequence getTitle() {
+ return mActionView.getTitle();
+ }
+
+ public CharSequence getSubtitle() {
+ return mActionView.getSubtitle();
+ }
+
+ public int getNavigationMode() {
+ return mActionView.getNavigationMode();
+ }
+
+ public int getDisplayOptions() {
+ return mActionView.getDisplayOptions();
+ }
+
+ @Override
+ public void startContextMode(ContextModeCallback callback) {
+ if (mContextMode != null) {
+ mContextMode.finish();
+ }
+
+ // Don't wait for the close context mode animation to finish.
+ if (mClosingContext) {
+ mAnimatorView.clearAnimation();
+ mHandler.removeCallbacks(mCloseContext);
+ mCloseContext.run();
+ }
+
+ mContextMode = new ContextMode(callback);
+ if (callback.onCreateContextMode(mContextMode, mContextMode.getMenu())) {
+ mContextMode.invalidate();
+ mUpperContextView.initForMode(mContextMode);
+ mAnimatorView.setDisplayedChild(CONTEXT_VIEW);
+ if (mLowerContextView != null) {
+ // TODO animate this
+ mLowerContextView.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void finishContextMode() {
+ if (mContextMode != null) {
+ mContextMode.finish();
+ }
+ }
+
+ private void configureTab(Tab tab, int position) {
+ final TabImpl tabi = (TabImpl) tab;
+ final boolean isFirstTab = mTabs.isEmpty();
+ final FragmentTransaction trans = mActivity.openFragmentTransaction();
+ final Fragment frag = tabi.getFragment();
+
+ tabi.setPosition(position);
+ mTabs.add(position, tabi);
+
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ if (!frag.isAdded()) {
+ trans.add(mTabContainerViewId, frag);
+ }
+ }
+
+ if (isFirstTab) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.show(frag);
+ } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) {
+ trans.add(mTabContainerViewId, frag);
+ }
+ mSelectedTab = tabi;
+ } else {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.hide(frag);
+ }
+ }
+ trans.commit();
+ }
+
+ @Override
+ public void addTab(Tab tab) {
+ mActionView.addTab(tab);
+ configureTab(tab, mTabs.size());
+ }
+
+ @Override
+ public void insertTab(Tab tab, int position) {
+ mActionView.insertTab(tab, position);
+ configureTab(tab, position);
+ }
+
+ @Override
+ public Tab newTab() {
+ return new TabImpl();
+ }
+
+ @Override
+ public void removeTab(Tab tab) {
+ removeTabAt(tab.getPosition());
+ }
+
+ @Override
+ public void removeTabAt(int position) {
+ mActionView.removeTabAt(position);
+ mTabs.remove(position);
+
+ final int newTabCount = mTabs.size();
+ for (int i = position; i < newTabCount; i++) {
+ mTabs.get(i).setPosition(i);
+ }
+
+ selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
+ }
+
+ @Override
+ public void setTabNavigationMode() {
+ mActionView.setNavigationMode(NAVIGATION_MODE_TABS);
+ }
+
+ @Override
+ public void setTabNavigationMode(int containerViewId) {
+ mTabContainerViewId = containerViewId;
+ setTabNavigationMode();
+ }
+
+ @Override
+ public void selectTab(Tab tab) {
+ if (mSelectedTab == tab) {
+ return;
+ }
+
+ mActionView.setTabSelected(tab != null ? tab.getPosition() : Tab.INVALID_POSITION);
+ final FragmentTransaction trans = mActivity.openFragmentTransaction();
+ if (mSelectedTab != null) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.hide(mSelectedTab.getFragment());
+ } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) {
+ trans.remove(mSelectedTab.getFragment());
+ }
+ }
+ if (tab != null) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.show(tab.getFragment());
+ } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) {
+ trans.add(mTabContainerViewId, tab.getFragment());
+ }
+ }
+ mSelectedTab = (TabImpl) tab;
+ trans.commit();
+ }
+
+ @Override
+ public void selectTabAt(int position) {
+ selectTab(mTabs.get(position));
+ }
+
+ /**
+ * @hide
+ */
+ public class ContextMode extends ActionBar.ContextMode {
+ private ContextModeCallback mCallback;
+ private ActionMenu mMenu;
+ private WeakReference<View> mCustomView;
+
+ public ContextMode(ContextModeCallback callback) {
+ mCallback = callback;
+ mMenu = new ActionMenu(mActionView.getContext());
+ }
+
+ @Override
+ public Menu getMenu() {
+ return mMenu;
+ }
+
+ @Override
+ public void finish() {
+ mCallback.onDestroyContextMode(this);
+ mAnimatorView.setDisplayedChild(NORMAL_VIEW);
+
+ // Clear out the context mode views after the animation finishes
+ mClosingContext = true;
+ mHandler.postDelayed(mCloseContext, mAnimatorView.getOutAnimation().getDuration());
+
+ if (mLowerContextView != null && mLowerContextView.getVisibility() != View.GONE) {
+ // TODO Animate this
+ mLowerContextView.setVisibility(View.GONE);
+ }
+ mContextMode = null;
+ }
+
+ @Override
+ public void invalidate() {
+ if (mCallback.onPrepareContextMode(this, mMenu)) {
+ // Refresh content in both context views
+ }
+ }
+
+ @Override
+ public void setCustomView(View view) {
+ mUpperContextView.setCustomView(view);
+ mCustomView = new WeakReference<View>(view);
+ }
+
+ @Override
+ public void setSubtitle(CharSequence subtitle) {
+ mUpperContextView.setSubtitle(subtitle);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ mUpperContextView.setTitle(title);
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return mUpperContextView.getTitle();
+ }
+
+ @Override
+ public CharSequence getSubtitle() {
+ return mUpperContextView.getSubtitle();
+ }
+
+ @Override
+ public View getCustomView() {
+ return mCustomView != null ? mCustomView.get() : null;
+ }
+
+ public void dispatchOnContextItemClicked(MenuItem item) {
+ ActionMenuItem actionItem = (ActionMenuItem) item;
+ if (!actionItem.invoke()) {
+ mCallback.onContextItemClicked(this, item);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public class TabImpl extends ActionBar.Tab {
+ private Fragment mFragment;
+ private Drawable mIcon;
+ private CharSequence mText;
+ private int mPosition;
+
+ @Override
+ public Fragment getFragment() {
+ return mFragment;
+ }
+
+ @Override
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ @Override
+ public int getPosition() {
+ return mPosition;
+ }
+
+ public void setPosition(int position) {
+ mPosition = position;
+ }
+
+ @Override
+ public CharSequence getText() {
+ return mText;
+ }
+
+ @Override
+ public void setFragment(Fragment fragment) {
+ mFragment = fragment;
+ }
+
+ @Override
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ @Override
+ public void setText(CharSequence text) {
+ mText = text;
+ }
+
+ @Override
+ public void select() {
+ selectTab(this);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/database/SortCursor.java b/core/java/com/android/internal/database/SortCursor.java
index 99410bc..0025512 100644
--- a/core/java/com/android/internal/database/SortCursor.java
+++ b/core/java/com/android/internal/database/SortCursor.java
@@ -182,24 +182,6 @@ public class SortCursor extends AbstractCursor
}
@Override
- public boolean deleteRow()
- {
- return mCursor.deleteRow();
- }
-
- @Override
- public boolean commitUpdates() {
- int length = mCursors.length;
- for (int i = 0 ; i < length ; i++) {
- if (mCursors[i] != null) {
- mCursors[i].commitUpdates();
- }
- }
- onChange(true);
- return true;
- }
-
- @Override
public String getString(int column)
{
return mCursor.getString(column);
@@ -236,6 +218,11 @@ public class SortCursor extends AbstractCursor
}
@Override
+ public int getType(int column) {
+ return mCursor.getType(column);
+ }
+
+ @Override
public boolean isNull(int column)
{
return mCursor.isNull(column);
diff --git a/core/java/com/android/internal/os/SamplingProfilerIntegration.java b/core/java/com/android/internal/os/SamplingProfilerIntegration.java
index 5f5c7a4..38362c1 100644
--- a/core/java/com/android/internal/os/SamplingProfilerIntegration.java
+++ b/core/java/com/android/internal/os/SamplingProfilerIntegration.java
@@ -16,14 +16,15 @@
package com.android.internal.os;
+import android.content.pm.PackageInfo;
import dalvik.system.SamplingProfiler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.FileNotFoundException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
import android.util.Log;
import android.os.*;
@@ -35,15 +36,27 @@ public class SamplingProfilerIntegration {
private static final String TAG = "SamplingProfilerIntegration";
+ public static final String SNAPSHOT_DIR = "/data/snapshots";
+
private static final boolean enabled;
private static final Executor snapshotWriter;
+ private static final int samplingProfilerHz;
+
+ /** Whether or not we've created the snapshots dir. */
+ private static boolean dirMade = false;
+
+ /** Whether or not a snapshot is being persisted. */
+ private static final AtomicBoolean pending = new AtomicBoolean(false);
+
static {
- enabled = "1".equals(SystemProperties.get("persist.sampling_profiler"));
- if (enabled) {
+ samplingProfilerHz = SystemProperties.getInt("persist.sys.profiler_hz", 0);
+ if (samplingProfilerHz > 0) {
snapshotWriter = Executors.newSingleThreadExecutor();
- Log.i(TAG, "Profiler is enabled.");
+ enabled = true;
+ Log.i(TAG, "Profiler is enabled. Sampling Profiler Hz: " + samplingProfilerHz);
} else {
snapshotWriter = null;
+ enabled = false;
Log.i(TAG, "Profiler is disabled.");
}
}
@@ -60,45 +73,45 @@ public class SamplingProfilerIntegration {
*/
public static void start() {
if (!enabled) return;
- SamplingProfiler.getInstance().start(10);
+ SamplingProfiler.getInstance().start(samplingProfilerHz);
}
- /** Whether or not we've created the snapshots dir. */
- static boolean dirMade = false;
-
- /** Whether or not a snapshot is being persisted. */
- static volatile boolean pending;
-
/**
- * Writes a snapshot to the SD card if profiling is enabled.
+ * Writes a snapshot if profiling is enabled.
*/
- public static void writeSnapshot(final String name) {
+ public static void writeSnapshot(final String processName, final PackageInfo packageInfo) {
if (!enabled) return;
/*
- * If we're already writing a snapshot, don't bother enqueing another
+ * If we're already writing a snapshot, don't bother enqueueing another
* request right now. This will reduce the number of individual
* snapshots and in turn the total amount of memory consumed (one big
* snapshot is smaller than N subset snapshots).
*/
- if (!pending) {
- pending = true;
+ if (pending.compareAndSet(false, true)) {
snapshotWriter.execute(new Runnable() {
public void run() {
- String dir = "/sdcard/snapshots";
if (!dirMade) {
- new File(dir).mkdirs();
- if (new File(dir).isDirectory()) {
+ File dir = new File(SNAPSHOT_DIR);
+ dir.mkdirs();
+ // the directory needs to be writable to anybody
+ dir.setWritable(true, false);
+ // the directory needs to be executable to anybody
+ // don't know why yet, but mode 723 would work, while
+ // mode 722 throws FileNotFoundExecption at line 151
+ dir.setExecutable(true, false);
+ if (new File(SNAPSHOT_DIR).isDirectory()) {
dirMade = true;
} else {
- Log.w(TAG, "Creation of " + dir + " failed.");
+ Log.w(TAG, "Creation of " + SNAPSHOT_DIR + " failed.");
+ pending.set(false);
return;
}
}
try {
- writeSnapshot(dir, name);
+ writeSnapshot(SNAPSHOT_DIR, processName, packageInfo);
} finally {
- pending = false;
+ pending.set(false);
}
}
});
@@ -110,13 +123,13 @@ public class SamplingProfilerIntegration {
*/
public static void writeZygoteSnapshot() {
if (!enabled) return;
-
- String dir = "/data/zygote/snapshots";
- new File(dir).mkdirs();
- writeSnapshot(dir, "zygote");
+ writeSnapshot("zygote", null);
}
- private static void writeSnapshot(String dir, String name) {
+ /**
+ * pass in PackageInfo to retrieve various values for snapshot header
+ */
+ private static void writeSnapshot(String dir, String processName, PackageInfo packageInfo) {
byte[] snapshot = SamplingProfiler.getInstance().snapshot();
if (snapshot == null) {
return;
@@ -128,39 +141,54 @@ public class SamplingProfilerIntegration {
* we capture two snapshots in rapid succession.
*/
long start = System.currentTimeMillis();
- String path = dir + "/" + name.replace(':', '.') + "-" +
- + System.currentTimeMillis() + ".snapshot";
+ String name = processName.replaceAll(":", ".");
+ String path = dir + "/" + name + "-" +System.currentTimeMillis() + ".snapshot";
+ FileOutputStream out = null;
try {
- // Try to open the file a few times. The SD card may not be mounted.
- FileOutputStream out;
- int count = 0;
- while (true) {
- try {
- out = new FileOutputStream(path);
- break;
- } catch (FileNotFoundException e) {
- if (++count > 3) {
- Log.e(TAG, "Could not open " + path + ".");
- return;
- }
-
- // Sleep for a bit and then try again.
- try {
- Thread.sleep(2500);
- } catch (InterruptedException e1) { /* ignore */ }
+ out = new FileOutputStream(path);
+ generateSnapshotHeader(name, packageInfo, out);
+ out.write(snapshot);
+ } catch (IOException e) {
+ Log.e(TAG, "Error writing snapshot.", e);
+ } finally {
+ try {
+ if(out != null) {
+ out.close();
}
+ } catch (IOException ex) {
+ // let it go.
}
+ }
+ // set file readable to the world so that SamplingProfilerService
+ // can put it to dropbox
+ new File(path).setReadable(true, false);
- try {
- out.write(snapshot);
- } finally {
- out.close();
- }
- long elapsed = System.currentTimeMillis() - start;
- Log.i(TAG, "Wrote snapshot for " + name
- + " in " + elapsed + "ms.");
- } catch (IOException e) {
- Log.e(TAG, "Error writing snapshot.", e);
+ long elapsed = System.currentTimeMillis() - start;
+ Log.i(TAG, "Wrote snapshot for " + name + " in " + elapsed + "ms.");
+ }
+
+ /**
+ * generate header for snapshots, with the following format (like http header):
+ *
+ * Version: <version number of profiler>\n
+ * Process: <process name>\n
+ * Package: <package name, if exists>\n
+ * Package-Version: <version number of the package, if exists>\n
+ * Build: <fingerprint>\n
+ * \n
+ * <the actual snapshot content begins here...>
+ */
+ private static void generateSnapshotHeader(String processName, PackageInfo packageInfo,
+ FileOutputStream out) throws IOException {
+ // profiler version
+ out.write("Version: 1\n".getBytes());
+ out.write(("Process: " + processName + "\n").getBytes());
+ if(packageInfo != null) {
+ out.write(("Package: " + packageInfo.packageName + "\n").getBytes());
+ out.write(("Package-Version: " + packageInfo.versionCode + "\n").getBytes());
}
+ out.write(("Build: " + Build.FINGERPRINT + "\n").getBytes());
+ // single blank line means the end of snapshot header.
+ out.write("\n".getBytes());
}
}
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index b677b1e..9fcd3f5 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -66,10 +66,6 @@ public class ZygoteInit {
/** when preloading, GC after allocating this many bytes */
private static final int PRELOAD_GC_THRESHOLD = 50000;
- /** throw on missing preload, only if this looks like a developer */
- private static final boolean THROW_ON_MISSING_PRELOAD =
- "1".equals(SystemProperties.get("persist.service.adb.enable"));
-
public static final String USAGE_STRING =
" <\"true\"|\"false\" for startSystemServer>";
@@ -287,7 +283,6 @@ public class ZygoteInit {
int count = 0;
String line;
- String missingClasses = null;
while ((line = br.readLine()) != null) {
// Skip comments and blank lines.
line = line.trim();
@@ -311,12 +306,7 @@ public class ZygoteInit {
}
count++;
} catch (ClassNotFoundException e) {
- Log.e(TAG, "Class not found for preloading: " + line);
- if (missingClasses == null) {
- missingClasses = line;
- } else {
- missingClasses += " " + line;
- }
+ Log.w(TAG, "Class not found for preloading: " + line);
} catch (Throwable t) {
Log.e(TAG, "Error preloading " + line + ".", t);
if (t instanceof Error) {
@@ -329,13 +319,6 @@ public class ZygoteInit {
}
}
- if (THROW_ON_MISSING_PRELOAD &&
- missingClasses != null) {
- throw new IllegalStateException(
- "Missing class(es) for preloading, update preloaded-classes ["
- + missingClasses + "]");
- }
-
Log.i(TAG, "...preloaded " + count + " classes in "
+ (SystemClock.uptimeMillis()-startTime) + "ms.");
} catch (IOException e) {
diff --git a/core/java/com/android/internal/util/HierarchicalStateMachine.java b/core/java/com/android/internal/util/HierarchicalStateMachine.java
index c599d68..7138b5c 100644
--- a/core/java/com/android/internal/util/HierarchicalStateMachine.java
+++ b/core/java/com/android/internal/util/HierarchicalStateMachine.java
@@ -1197,6 +1197,35 @@ public class HierarchicalStateMachine {
}
/**
+ * Get a message and set Message.target = this,
+ * what, arg1 and arg2
+ *
+ * @param what is assigned to Message.what
+ * @param arg1 is assigned to Message.arg1
+ * @param arg2 is assigned to Message.arg2
+ * @return A Message object from the global pool.
+ */
+ public final Message obtainMessage(int what, int arg1, int arg2)
+ {
+ return Message.obtain(mHsmHandler, what, arg1, arg2);
+ }
+
+ /**
+ * Get a message and set Message.target = this,
+ * what, arg1, arg2 and obj
+ *
+ * @param what is assigned to Message.what
+ * @param arg1 is assigned to Message.arg1
+ * @param arg2 is assigned to Message.arg2
+ * @param obj is assigned to Message.obj
+ * @return A Message object from the global pool.
+ */
+ public final Message obtainMessage(int what, int arg1, int arg2, Object obj)
+ {
+ return Message.obtain(mHsmHandler, what, arg1, arg2, obj);
+ }
+
+ /**
* Enqueue a message to this state machine.
*/
public final void sendMessage(int what) {
diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java
index 8d8df16..e00a853 100644
--- a/core/java/com/android/internal/util/XmlUtils.java
+++ b/core/java/com/android/internal/util/XmlUtils.java
@@ -26,6 +26,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -284,6 +285,26 @@ public class XmlUtils
out.endTag(null, "list");
}
+
+ public static final void writeSetXml(Set val, String name, XmlSerializer out)
+ throws XmlPullParserException, java.io.IOException {
+ if (val == null) {
+ out.startTag(null, "null");
+ out.endTag(null, "null");
+ return;
+ }
+
+ out.startTag(null, "set");
+ if (name != null) {
+ out.attribute(null, "name", name);
+ }
+
+ for (Object v : val) {
+ writeValueXml(v, null, out);
+ }
+
+ out.endTag(null, "set");
+ }
/**
* Flatten a byte[] into an XmlSerializer. The list can later be read back
@@ -426,6 +447,9 @@ public class XmlUtils
} else if (v instanceof List) {
writeListXml((List)v, name, out);
return;
+ } else if (v instanceof Set) {
+ writeSetXml((Set)v, name, out);
+ return;
} else if (v instanceof CharSequence) {
// XXX This is to allow us to at least write something if
// we encounter styled text... but it means we will drop all
@@ -476,7 +500,7 @@ public class XmlUtils
*
* @param in The InputStream from which to read.
*
- * @return HashMap The resulting list.
+ * @return ArrayList The resulting list.
*
* @see #readMapXml
* @see #readValueXml
@@ -490,6 +514,29 @@ public class XmlUtils
parser.setInput(in, null);
return (ArrayList)readValueXml(parser, new String[1]);
}
+
+
+ /**
+ * Read a HashSet from an InputStream containing XML. The stream can
+ * previously have been written by writeSetXml().
+ *
+ * @param in The InputStream from which to read.
+ *
+ * @return HashSet The resulting set.
+ *
+ * @throws XmlPullParserException
+ * @throws java.io.IOException
+ *
+ * @see #readValueXml
+ * @see #readThisSetXml
+ * @see #writeSetXml
+ */
+ public static final HashSet readSetXml(InputStream in)
+ throws XmlPullParserException, java.io.IOException {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(in, null);
+ return (HashSet) readValueXml(parser, new String[1]);
+ }
/**
* Read a HashMap object from an XmlPullParser. The XML data could
@@ -573,6 +620,47 @@ public class XmlUtils
throw new XmlPullParserException(
"Document ended before " + endTag + " end tag");
}
+
+ /**
+ * Read a HashSet object from an XmlPullParser. The XML data could previously
+ * have been generated by writeSetXml(). The XmlPullParser must be positioned
+ * <em>after</em> the tag that begins the set.
+ *
+ * @param parser The XmlPullParser from which to read the set data.
+ * @param endTag Name of the tag that will end the set, usually "set".
+ * @param name An array of one string, used to return the name attribute
+ * of the set's tag.
+ *
+ * @return HashSet The newly generated set.
+ *
+ * @throws XmlPullParserException
+ * @throws java.io.IOException
+ *
+ * @see #readSetXml
+ */
+ public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name)
+ throws XmlPullParserException, java.io.IOException {
+ HashSet set = new HashSet();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == parser.START_TAG) {
+ Object val = readThisValueXml(parser, name);
+ set.add(val);
+ //System.out.println("Adding to set: " + val);
+ } else if (eventType == parser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return set;
+ }
+ throw new XmlPullParserException(
+ "Expected " + endTag + " end tag at: " + parser.getName());
+ }
+ eventType = parser.next();
+ } while (eventType != parser.END_DOCUMENT);
+
+ throw new XmlPullParserException(
+ "Document ended before " + endTag + " end tag");
+ }
/**
* Read an int[] object from an XmlPullParser. The XML data could
@@ -740,6 +828,12 @@ public class XmlUtils
name[0] = valueName;
//System.out.println("Returning value for " + valueName + ": " + res);
return res;
+ } else if (tagName.equals("set")) {
+ parser.next();
+ res = readThisSetXml(parser, "set", name);
+ name[0] = valueName;
+ //System.out.println("Returning value for " + valueName + ": " + res);
+ return res;
} else {
throw new XmlPullParserException(
"Unknown tag: " + tagName);
diff --git a/core/java/com/android/internal/view/menu/ActionMenu.java b/core/java/com/android/internal/view/menu/ActionMenu.java
new file mode 100644
index 0000000..3d44ebc
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/ActionMenu.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view.menu;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+
+/**
+ * @hide
+ */
+public class ActionMenu implements Menu {
+ private Context mContext;
+
+ private boolean mIsQwerty;
+
+ private ArrayList<ActionMenuItem> mItems;
+
+ public ActionMenu(Context context) {
+ mContext = context;
+ mItems = new ArrayList<ActionMenuItem>();
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public MenuItem add(CharSequence title) {
+ return add(0, 0, 0, title);
+ }
+
+ public MenuItem add(int titleRes) {
+ return add(0, 0, 0, titleRes);
+ }
+
+ public MenuItem add(int groupId, int itemId, int order, int titleRes) {
+ return add(groupId, itemId, order, mContext.getResources().getString(titleRes));
+ }
+
+ public MenuItem add(int groupId, int itemId, int order, CharSequence title) {
+ ActionMenuItem item = new ActionMenuItem(getContext(),
+ groupId, itemId, 0, order, title);
+ mItems.add(order, item);
+ return item;
+ }
+
+ public int addIntentOptions(int groupId, int itemId, int order,
+ ComponentName caller, Intent[] specifics, Intent intent, int flags,
+ MenuItem[] outSpecificItems) {
+ PackageManager pm = mContext.getPackageManager();
+ final List<ResolveInfo> lri =
+ pm.queryIntentActivityOptions(caller, specifics, intent, 0);
+ final int N = lri != null ? lri.size() : 0;
+
+ if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
+ removeGroup(groupId);
+ }
+
+ for (int i=0; i<N; i++) {
+ final ResolveInfo ri = lri.get(i);
+ Intent rintent = new Intent(
+ ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
+ rintent.setComponent(new ComponentName(
+ ri.activityInfo.applicationInfo.packageName,
+ ri.activityInfo.name));
+ final MenuItem item = add(groupId, itemId, order, ri.loadLabel(pm))
+ .setIcon(ri.loadIcon(pm))
+ .setIntent(rintent);
+ if (outSpecificItems != null && ri.specificIndex >= 0) {
+ outSpecificItems[ri.specificIndex] = item;
+ }
+ }
+
+ return N;
+ }
+
+ public SubMenu addSubMenu(CharSequence title) {
+ // TODO Implement submenus
+ return null;
+ }
+
+ public SubMenu addSubMenu(int titleRes) {
+ // TODO Implement submenus
+ return null;
+ }
+
+ public SubMenu addSubMenu(int groupId, int itemId, int order,
+ CharSequence title) {
+ // TODO Implement submenus
+ return null;
+ }
+
+ public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) {
+ // TODO Implement submenus
+ return null;
+ }
+
+ public void clear() {
+ mItems.clear();
+ }
+
+ public void close() {
+ }
+
+ private int findItemIndex(int id) {
+ final ArrayList<ActionMenuItem> items = mItems;
+ final int itemCount = items.size();
+ for (int i = 0; i < itemCount; i++) {
+ if (items.get(i).getItemId() == id) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public MenuItem findItem(int id) {
+ return mItems.get(findItemIndex(id));
+ }
+
+ public MenuItem getItem(int index) {
+ return mItems.get(index);
+ }
+
+ public boolean hasVisibleItems() {
+ final ArrayList<ActionMenuItem> items = mItems;
+ final int itemCount = items.size();
+
+ for (int i = 0; i < itemCount; i++) {
+ if (items.get(i).isVisible()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private ActionMenuItem findItemWithShortcut(int keyCode, KeyEvent event) {
+ // TODO Make this smarter.
+ final boolean qwerty = mIsQwerty;
+ final ArrayList<ActionMenuItem> items = mItems;
+ final int itemCount = items.size();
+
+ for (int i = 0; i < itemCount; i++) {
+ ActionMenuItem item = items.get(i);
+ final char shortcut = qwerty ? item.getAlphabeticShortcut() :
+ item.getNumericShortcut();
+ if (keyCode == shortcut) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ public boolean isShortcutKey(int keyCode, KeyEvent event) {
+ return findItemWithShortcut(keyCode, event) != null;
+ }
+
+ public boolean performIdentifierAction(int id, int flags) {
+ final int index = findItemIndex(id);
+ if (index < 0) {
+ return false;
+ }
+
+ return mItems.get(index).invoke();
+ }
+
+ public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
+ ActionMenuItem item = findItemWithShortcut(keyCode, event);
+ if (item == null) {
+ return false;
+ }
+
+ return item.invoke();
+ }
+
+ public void removeGroup(int groupId) {
+ final ArrayList<ActionMenuItem> items = mItems;
+ int itemCount = items.size();
+ int i = 0;
+ while (i < itemCount) {
+ if (items.get(i).getGroupId() == groupId) {
+ items.remove(i);
+ itemCount--;
+ } else {
+ i++;
+ }
+ }
+ }
+
+ public void removeItem(int id) {
+ mItems.remove(findItemIndex(id));
+ }
+
+ public void setGroupCheckable(int group, boolean checkable,
+ boolean exclusive) {
+ final ArrayList<ActionMenuItem> items = mItems;
+ final int itemCount = items.size();
+
+ for (int i = 0; i < itemCount; i++) {
+ ActionMenuItem item = items.get(i);
+ if (item.getGroupId() == group) {
+ item.setCheckable(checkable);
+ item.setExclusiveCheckable(exclusive);
+ }
+ }
+ }
+
+ public void setGroupEnabled(int group, boolean enabled) {
+ final ArrayList<ActionMenuItem> items = mItems;
+ final int itemCount = items.size();
+
+ for (int i = 0; i < itemCount; i++) {
+ ActionMenuItem item = items.get(i);
+ if (item.getGroupId() == group) {
+ item.setEnabled(enabled);
+ }
+ }
+ }
+
+ public void setGroupVisible(int group, boolean visible) {
+ final ArrayList<ActionMenuItem> items = mItems;
+ final int itemCount = items.size();
+
+ for (int i = 0; i < itemCount; i++) {
+ ActionMenuItem item = items.get(i);
+ if (item.getGroupId() == group) {
+ item.setVisible(visible);
+ }
+ }
+ }
+
+ public void setQwertyMode(boolean isQwerty) {
+ mIsQwerty = isQwerty;
+ }
+
+ public int size() {
+ return mItems.size();
+ }
+}
diff --git a/core/java/com/android/internal/view/menu/ActionMenuItem.java b/core/java/com/android/internal/view/menu/ActionMenuItem.java
new file mode 100644
index 0000000..035875a
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/ActionMenuItem.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view.menu;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+
+/**
+ * @hide
+ */
+public class ActionMenuItem implements MenuItem {
+ private final int mId;
+ private final int mGroup;
+ private final int mCategoryOrder;
+ private final int mOrdering;
+
+ private CharSequence mTitle;
+ private CharSequence mTitleCondensed;
+ private Intent mIntent;
+ private char mShortcutNumericChar;
+ private char mShortcutAlphabeticChar;
+
+ private Drawable mIconDrawable;
+ private int mIconResId = NO_ICON;
+
+ private Context mContext;
+
+ private MenuItem.OnMenuItemClickListener mClickListener;
+
+ private static final int NO_ICON = 0;
+
+ private int mFlags = ENABLED;
+ private static final int CHECKABLE = 0x00000001;
+ private static final int CHECKED = 0x00000002;
+ private static final int EXCLUSIVE = 0x00000004;
+ private static final int HIDDEN = 0x00000008;
+ private static final int ENABLED = 0x00000010;
+
+ public ActionMenuItem(Context context, int group, int id, int categoryOrder, int ordering,
+ CharSequence title) {
+ mContext = context;
+ mId = id;
+ mGroup = group;
+ mCategoryOrder = categoryOrder;
+ mOrdering = ordering;
+ mTitle = title;
+ }
+
+ public char getAlphabeticShortcut() {
+ return mShortcutAlphabeticChar;
+ }
+
+ public int getGroupId() {
+ return mGroup;
+ }
+
+ public Drawable getIcon() {
+ return mIconDrawable;
+ }
+
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ public int getItemId() {
+ return mId;
+ }
+
+ public ContextMenuInfo getMenuInfo() {
+ return null;
+ }
+
+ public char getNumericShortcut() {
+ return mShortcutNumericChar;
+ }
+
+ public int getOrder() {
+ return mOrdering;
+ }
+
+ public SubMenu getSubMenu() {
+ return null;
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public CharSequence getTitleCondensed() {
+ return mTitleCondensed;
+ }
+
+ public boolean hasSubMenu() {
+ return false;
+ }
+
+ public boolean isCheckable() {
+ return (mFlags & CHECKABLE) != 0;
+ }
+
+ public boolean isChecked() {
+ return (mFlags & CHECKED) != 0;
+ }
+
+ public boolean isEnabled() {
+ return (mFlags & ENABLED) != 0;
+ }
+
+ public boolean isVisible() {
+ return (mFlags & HIDDEN) == 0;
+ }
+
+ public MenuItem setAlphabeticShortcut(char alphaChar) {
+ mShortcutAlphabeticChar = alphaChar;
+ return this;
+ }
+
+ public MenuItem setCheckable(boolean checkable) {
+ mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
+ return this;
+ }
+
+ public ActionMenuItem setExclusiveCheckable(boolean exclusive) {
+ mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
+ return this;
+ }
+
+ public MenuItem setChecked(boolean checked) {
+ mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
+ return this;
+ }
+
+ public MenuItem setEnabled(boolean enabled) {
+ mFlags = (mFlags & ~ENABLED) | (enabled ? ENABLED : 0);
+ return this;
+ }
+
+ public MenuItem setIcon(Drawable icon) {
+ mIconDrawable = icon;
+ mIconResId = NO_ICON;
+ return this;
+ }
+
+ public MenuItem setIcon(int iconRes) {
+ mIconResId = iconRes;
+ mIconDrawable = mContext.getResources().getDrawable(iconRes);
+ return this;
+ }
+
+ public MenuItem setIntent(Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ public MenuItem setNumericShortcut(char numericChar) {
+ mShortcutNumericChar = numericChar;
+ return this;
+ }
+
+ public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) {
+ mClickListener = menuItemClickListener;
+ return this;
+ }
+
+ public MenuItem setShortcut(char numericChar, char alphaChar) {
+ mShortcutNumericChar = numericChar;
+ mShortcutAlphabeticChar = alphaChar;
+ return this;
+ }
+
+ public MenuItem setTitle(CharSequence title) {
+ mTitle = title;
+ return this;
+ }
+
+ public MenuItem setTitle(int title) {
+ mTitle = mContext.getResources().getString(title);
+ return this;
+ }
+
+ public MenuItem setTitleCondensed(CharSequence title) {
+ mTitleCondensed = title;
+ return this;
+ }
+
+ public MenuItem setVisible(boolean visible) {
+ mFlags = (mFlags & HIDDEN) | (visible ? 0 : HIDDEN);
+ return this;
+ }
+
+ public boolean invoke() {
+ if (mClickListener != null && mClickListener.onMenuItemClick(this)) {
+ return true;
+ }
+
+ if (mIntent != null) {
+ mContext.startActivity(mIntent);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void setShowAsAction(int show) {
+ // Do nothing. ActionMenuItems always show as action buttons.
+ }
+}
diff --git a/core/java/com/android/internal/view/menu/ActionMenuItemView.java b/core/java/com/android/internal/view/menu/ActionMenuItemView.java
new file mode 100644
index 0000000..f0d9f60
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/ActionMenuItemView.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view.menu;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.widget.ImageButton;
+
+/**
+ * @hide
+ */
+public class ActionMenuItemView extends ImageButton implements MenuView.ItemView {
+ private static final String TAG = "ActionMenuItemView";
+
+ private MenuItemImpl mItemData;
+ private CharSequence mTitle;
+ private MenuBuilder.ItemInvoker mItemInvoker;
+
+ public ActionMenuItemView(Context context) {
+ this(context, null);
+ }
+
+ public ActionMenuItemView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.actionButtonStyle);
+ }
+
+ public ActionMenuItemView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public MenuItemImpl getItemData() {
+ return mItemData;
+ }
+
+ public void initialize(MenuItemImpl itemData, int menuType) {
+ mItemData = itemData;
+
+ setClickable(true);
+ setFocusable(true);
+ setTitle(itemData.getTitle());
+ setIcon(itemData.getIcon());
+ setId(itemData.getItemId());
+
+ setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
+ setEnabled(itemData.isEnabled());
+ }
+
+ @Override
+ public boolean performClick() {
+ // Let the view's listener have top priority
+ if (super.performClick()) {
+ return true;
+ }
+
+ if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void setItemInvoker(MenuBuilder.ItemInvoker invoker) {
+ mItemInvoker = invoker;
+ }
+
+ public boolean prefersCondensedTitle() {
+ return false;
+ }
+
+ public void setCheckable(boolean checkable) {
+ // TODO Support checkable action items
+ }
+
+ public void setChecked(boolean checked) {
+ // TODO Support checkable action items
+ }
+
+ public void setIcon(Drawable icon) {
+ setImageDrawable(icon);
+ }
+
+ public void setShortcut(boolean showShortcut, char shortcutKey) {
+ // Action buttons don't show text for shortcut keys.
+ }
+
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ }
+
+ public boolean showsIcon() {
+ return true;
+ }
+
+}
diff --git a/core/java/com/android/internal/view/menu/ActionMenuView.java b/core/java/com/android/internal/view/menu/ActionMenuView.java
new file mode 100644
index 0000000..c3fe5dc
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/ActionMenuView.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.view.menu;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView {
+ private static final String TAG = "ActionMenuView";
+
+ private MenuBuilder mMenu;
+
+ private int mItemPadding;
+ private int mItemMargin;
+ private int mMaxItems;
+
+ public ActionMenuView(Context context) {
+ this(context, null);
+ }
+
+ public ActionMenuView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Theme);
+ mItemPadding = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.Theme_actionButtonPadding, 0);
+ mItemMargin = mItemPadding / 2;
+ a.recycle();
+
+ final Resources res = getResources();
+ final int size = res.getDimensionPixelSize(com.android.internal.R.dimen.action_icon_size);
+ final int spaceAvailable = res.getDisplayMetrics().widthPixels / 2;
+ final int itemSpace = size + mItemPadding;
+
+ mMaxItems = spaceAvailable / (itemSpace > 0 ? itemSpace : 1);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ if (p instanceof LayoutParams) {
+ LayoutParams lp = (LayoutParams) p;
+ return lp.leftMargin == mItemMargin && lp.rightMargin == mItemMargin &&
+ lp.width == LayoutParams.WRAP_CONTENT && lp.height == LayoutParams.WRAP_CONTENT;
+ }
+ return false;
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ params.leftMargin = mItemMargin;
+ params.rightMargin = mItemMargin;
+ return params;
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return generateDefaultLayoutParams();
+ }
+
+ public int getItemMargin() {
+ return mItemMargin;
+ }
+
+ public boolean invokeItem(MenuItemImpl item) {
+ return mMenu.performItemAction(item, 0);
+ }
+
+ public int getWindowAnimations() {
+ return 0;
+ }
+
+ public void initialize(MenuBuilder menu, int menuType) {
+ menu.setMaxActionItems(mMaxItems);
+ mMenu = menu;
+ updateChildren(true);
+ }
+
+ public void updateChildren(boolean cleared) {
+ removeAllViews();
+
+ final ArrayList<MenuItemImpl> itemsToShow = mMenu.getActionItems();
+ final int itemCount = itemsToShow.size();
+
+ for (int i = 0; i < itemCount; i++) {
+ final MenuItemImpl itemData = itemsToShow.get(i);
+ addItemView((ActionMenuItemView) itemData.getItemView(MenuBuilder.TYPE_ACTION_BUTTON,
+ this));
+ }
+ }
+
+ private void addItemView(ActionMenuItemView view) {
+ view.setItemInvoker(this);
+ addView(view);
+ }
+}
diff --git a/core/java/com/android/internal/view/menu/IconMenuView.java b/core/java/com/android/internal/view/menu/IconMenuView.java
index beb57ba..bbf7c68 100644
--- a/core/java/com/android/internal/view/menu/IconMenuView.java
+++ b/core/java/com/android/internal/view/menu/IconMenuView.java
@@ -337,7 +337,7 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi
// This method does a clear refresh of children
removeAllViews();
- final ArrayList<MenuItemImpl> itemsToShow = mMenu.getVisibleItems();
+ final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems();
final int numItems = itemsToShow.size();
final int numItemsThatCanFit = mMaxItems;
// Minimum of the num that can fit and the num that we have
diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java
index 228d5d0..94a9f65 100644
--- a/core/java/com/android/internal/view/menu/MenuBuilder.java
+++ b/core/java/com/android/internal/view/menu/MenuBuilder.java
@@ -27,16 +27,17 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
+import android.util.Log;
import android.util.SparseArray;
import android.view.ContextThemeWrapper;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
-import android.view.LayoutInflater;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
@@ -54,7 +55,7 @@ public class MenuBuilder implements Menu {
private static final String LOGTAG = "MenuBuilder";
/** The number of different menu types */
- public static final int NUM_TYPES = 3;
+ public static final int NUM_TYPES = 5;
/** The menu type that represents the icon menu view */
public static final int TYPE_ICON = 0;
/** The menu type that represents the expanded menu view */
@@ -65,14 +66,24 @@ public class MenuBuilder implements Menu {
* have an ItemView.
*/
public static final int TYPE_DIALOG = 2;
+ /**
+ * The menu type that represents a button in the application's action bar.
+ */
+ public static final int TYPE_ACTION_BUTTON = 3;
+ /**
+ * The menu type that represents a menu popup.
+ */
+ public static final int TYPE_POPUP = 4;
private static final String VIEWS_TAG = "android:views";
-
+
// Order must be the same order as the TYPE_*
static final int THEME_RES_FOR_TYPE[] = new int[] {
com.android.internal.R.style.Theme_IconMenu,
com.android.internal.R.style.Theme_ExpandedMenu,
0,
+ 0,
+ 0,
};
// Order must be the same order as the TYPE_*
@@ -80,6 +91,8 @@ public class MenuBuilder implements Menu {
com.android.internal.R.layout.icon_menu_layout,
com.android.internal.R.layout.expanded_menu_layout,
0,
+ com.android.internal.R.layout.action_menu_layout,
+ 0,
};
// Order must be the same order as the TYPE_*
@@ -87,6 +100,8 @@ public class MenuBuilder implements Menu {
com.android.internal.R.layout.icon_menu_item_layout,
com.android.internal.R.layout.list_menu_item_layout,
com.android.internal.R.layout.list_menu_item_layout,
+ com.android.internal.R.layout.action_menu_item_layout,
+ com.android.internal.R.layout.list_menu_item_layout,
};
private static final int[] sCategoryToOrder = new int[] {
@@ -130,6 +145,24 @@ public class MenuBuilder implements Menu {
* fetched from {@link #getVisibleItems()}
*/
private boolean mIsVisibleItemsStale;
+
+ /**
+ * Contains only the items that should appear in the Action Bar, if present.
+ */
+ private ArrayList<MenuItemImpl> mActionItems;
+ /**
+ * Contains items that should NOT appear in the Action Bar, if present.
+ */
+ private ArrayList<MenuItemImpl> mNonActionItems;
+ /**
+ * The number of visible action buttons permitted in this menu
+ */
+ private int mMaxActionItems;
+ /**
+ * Whether or not the items (or any one item's action state) has changed since it was
+ * last fetched.
+ */
+ private boolean mIsActionItemsStale;
/**
* Current use case is Context Menus: As Views populate the context menu, each one has
@@ -281,6 +314,10 @@ public class MenuBuilder implements Menu {
mVisibleItems = new ArrayList<MenuItemImpl>();
mIsVisibleItemsStale = true;
+ mActionItems = new ArrayList<MenuItemImpl>();
+ mNonActionItems = new ArrayList<MenuItemImpl>();
+ mIsActionItemsStale = true;
+
mShortcutsVisible =
(mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS);
}
@@ -900,6 +937,7 @@ public class MenuBuilder implements Menu {
private void onItemsChanged(boolean cleared) {
if (!mPreventDispatchingItemsChanged) {
if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true;
+ if (mIsActionItemsStale == false) mIsActionItemsStale = true;
MenuType[] menuTypes = mMenuTypes;
for (int i = 0; i < NUM_TYPES; i++) {
@@ -920,6 +958,15 @@ public class MenuBuilder implements Menu {
onItemsChanged(false);
}
+ /**
+ * Called by {@link MenuItemImpl} when its action request status is changed.
+ * @param item The item that has gone through a change in action request status.
+ */
+ void onItemActionRequestChanged(MenuItemImpl item) {
+ // Notify of items being changed
+ onItemsChanged(false);
+ }
+
ArrayList<MenuItemImpl> getVisibleItems() {
if (!mIsVisibleItemsStale) return mVisibleItems;
@@ -934,9 +981,64 @@ public class MenuBuilder implements Menu {
}
mIsVisibleItemsStale = false;
+ mIsActionItemsStale = true;
return mVisibleItems;
}
+
+ private void flagActionItems() {
+ if (!mIsActionItemsStale) {
+ return;
+ }
+
+ final ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
+ final int itemsSize = visibleItems.size();
+ int maxActions = mMaxActionItems;
+
+ for (int i = 0; i < itemsSize; i++) {
+ MenuItemImpl item = visibleItems.get(i);
+ if (item.requiresActionButton()) {
+ maxActions--;
+ }
+ }
+
+ // Flag as many more requested items as will fit.
+ for (int i = 0; i < itemsSize; i++) {
+ MenuItemImpl item = visibleItems.get(i);
+ if (item.requestsActionButton()) {
+ item.setIsActionButton(maxActions > 0);
+ maxActions--;
+ }
+ }
+
+ mActionItems.clear();
+ mNonActionItems.clear();
+ for (int i = 0; i < itemsSize; i++) {
+ MenuItemImpl item = visibleItems.get(i);
+ if (item.isActionButton()) {
+ mActionItems.add(item);
+ } else {
+ mNonActionItems.add(item);
+ }
+ }
+
+ mIsActionItemsStale = false;
+ }
+
+ ArrayList<MenuItemImpl> getActionItems() {
+ flagActionItems();
+ return mActionItems;
+ }
+
+ ArrayList<MenuItemImpl> getNonActionItems() {
+ flagActionItems();
+ return mNonActionItems;
+ }
+
+ void setMaxActionItems(int maxActionItems) {
+ mMaxActionItems = maxActionItems;
+ mIsActionItemsStale = true;
+ }
public void clearHeader() {
mHeaderIcon = null;
@@ -1155,7 +1257,19 @@ public class MenuBuilder implements Menu {
}
public View getView(int position, View convertView, ViewGroup parent) {
- return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent);
+ if (convertView != null) {
+ MenuView.ItemView itemView = (MenuView.ItemView) convertView;
+ itemView.getItemData().setItemView(mMenuType, null);
+
+ MenuItemImpl item = (MenuItemImpl) getItem(position);
+ itemView.initialize(item, mMenuType);
+ item.setItemView(mMenuType, itemView);
+ return convertView;
+ } else {
+ MenuItemImpl item = (MenuItemImpl) getItem(position);
+ item.setItemView(mMenuType, null);
+ return item.getItemView(mMenuType, parent);
+ }
}
}
diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java
index 9b58205..fecbd77 100644
--- a/core/java/com/android/internal/view/menu/MenuItemImpl.java
+++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java
@@ -74,6 +74,9 @@ public final class MenuItemImpl implements MenuItem {
private static final int EXCLUSIVE = 0x00000004;
private static final int HIDDEN = 0x00000008;
private static final int ENABLED = 0x00000010;
+ private static final int IS_ACTION = 0x00000020;
+
+ private int mShowAsAction = SHOW_AS_ACTION_NEVER;
/** Used for the icon resource ID if this item does not have an icon */
static final int NO_ICON = 0;
@@ -580,6 +583,10 @@ public final class MenuItemImpl implements MenuItem {
return (View) mItemViews[menuType].get();
}
+ void setItemView(int menuType, ItemView view) {
+ mItemViews[menuType] = new WeakReference<ItemView>(view);
+ }
+
/**
* Create and initializes a menu item view that implements {@link MenuView.ItemView}.
* @param menuType The type of menu to get a View for (must be one of
@@ -628,6 +635,34 @@ public final class MenuItemImpl implements MenuItem {
* @return Whether the given menu type should show icons for menu items.
*/
public boolean shouldShowIcon(int menuType) {
- return menuType == MenuBuilder.TYPE_ICON || mMenu.getOptionalIconsVisible();
+ return menuType == MenuBuilder.TYPE_ICON ||
+ menuType == MenuBuilder.TYPE_ACTION_BUTTON ||
+ menuType == MenuBuilder.TYPE_POPUP ||
+ mMenu.getOptionalIconsVisible();
+ }
+
+ public boolean isActionButton() {
+ return (mFlags & IS_ACTION) == IS_ACTION || requiresActionButton();
+ }
+
+ public boolean requestsActionButton() {
+ return mShowAsAction == SHOW_AS_ACTION_IF_ROOM;
+ }
+
+ public boolean requiresActionButton() {
+ return mShowAsAction == SHOW_AS_ACTION_ALWAYS;
+ }
+
+ public void setIsActionButton(boolean isActionButton) {
+ if (isActionButton) {
+ mFlags |= IS_ACTION;
+ } else {
+ mFlags &= ~IS_ACTION;
+ }
+ }
+
+ public void setShowAsAction(int actionEnum) {
+ mShowAsAction = actionEnum;
+ mMenu.onItemActionRequestChanged(this);
}
}
diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
new file mode 100644
index 0000000..751ecda
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view.menu;
+
+import com.android.internal.view.menu.MenuBuilder.MenuAdapter;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.widget.AdapterView;
+import android.widget.ListPopupWindow;
+
+/**
+ * @hide
+ */
+public class MenuPopupHelper implements AdapterView.OnItemClickListener {
+ private static final String TAG = "MenuPopupHelper";
+
+ private Context mContext;
+ private ListPopupWindow mPopup;
+ private SubMenuBuilder mSubMenu;
+ private int mPopupMaxWidth;
+
+ public MenuPopupHelper(Context context, SubMenuBuilder subMenu) {
+ mContext = context;
+ mSubMenu = subMenu;
+
+ final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ mPopupMaxWidth = metrics.widthPixels / 2;
+ }
+
+ public void show() {
+ // TODO Use a style from the theme here
+ mPopup = new ListPopupWindow(mContext, null, 0,
+ com.android.internal.R.style.Widget_Spinner);
+ mPopup.setOnItemClickListener(this);
+
+ final MenuAdapter adapter = mSubMenu.getMenuAdapter(MenuBuilder.TYPE_POPUP);
+ mPopup.setAdapter(adapter);
+ mPopup.setModal(true);
+
+ final MenuItemImpl itemImpl = (MenuItemImpl) mSubMenu.getItem();
+ final View anchorView = itemImpl.getItemView(MenuBuilder.TYPE_ACTION_BUTTON, null);
+ mPopup.setAnchorView(anchorView);
+
+ mPopup.setContentWidth(Math.min(measureContentWidth(adapter), mPopupMaxWidth));
+ mPopup.show();
+ }
+
+ public void dismiss() {
+ mPopup.dismiss();
+ mPopup = null;
+ }
+
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ mSubMenu.performItemAction(mSubMenu.getItem(position), 0);
+ mPopup.dismiss();
+ }
+
+ private int measureContentWidth(MenuAdapter adapter) {
+ // Menus don't tend to be long, so this is more sane than it looks.
+ int width = 0;
+ View itemView = null;
+ final int widthMeasureSpec =
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int heightMeasureSpec =
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ itemView = adapter.getView(i, itemView, null);
+ itemView.measure(widthMeasureSpec, heightMeasureSpec);
+ width = Math.max(width, itemView.getMeasuredWidth());
+ }
+ return width;
+ }
+}
diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java
new file mode 100644
index 0000000..b57b7a8
--- /dev/null
+++ b/core/java/com/android/internal/widget/ActionBarContextView.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget;
+
+import com.android.internal.R;
+import com.android.internal.app.ActionBarImpl;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * @hide
+ */
+public class ActionBarContextView extends ViewGroup {
+ // TODO: This must be defined in the default theme
+ private static final int CONTENT_HEIGHT_DIP = 50;
+
+ private int mItemPadding;
+ private int mItemMargin;
+ private int mContentHeight;
+
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+
+ private ImageButton mCloseButton;
+ private View mCustomView;
+ private LinearLayout mTitleLayout;
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private Drawable mCloseDrawable;
+
+ public ActionBarContextView(Context context) {
+ this(context, null, 0);
+ }
+
+ public ActionBarContextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ActionBarContextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Theme);
+ mItemPadding = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.Theme_actionButtonPadding, 0);
+ setBackgroundDrawable(a.getDrawable(
+ com.android.internal.R.styleable.Theme_actionBarContextBackground));
+ mCloseDrawable = a.getDrawable(
+ com.android.internal.R.styleable.Theme_actionBarCloseContextDrawable);
+ mItemMargin = mItemPadding / 2;
+
+ mContentHeight = CONTENT_HEIGHT_DIP;
+ a.recycle();
+ }
+
+ public void setCustomView(View view) {
+ if (mCustomView != null) {
+ removeView(mCustomView);
+ }
+ mCustomView = view;
+ if (mTitleLayout != null) {
+ removeView(mTitleLayout);
+ mTitleLayout = null;
+ }
+ if (view != null) {
+ addView(view);
+ }
+ requestLayout();
+ }
+
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ initTitle();
+ }
+
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ initTitle();
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ private void initTitle() {
+ if (mTitleLayout == null) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ mTitleLayout = (LinearLayout) inflater.inflate(R.layout.action_bar_title_item, null);
+ mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
+ mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
+ if (mTitle != null) {
+ mTitleView.setText(mTitle);
+ }
+ if (mSubtitle != null) {
+ mSubtitleView.setText(mSubtitle);
+ }
+ addView(mTitleLayout);
+ } else {
+ mTitleView.setText(mTitle);
+ mSubtitleView.setText(mSubtitle);
+ if (mTitleLayout.getParent() == null) {
+ addView(mTitleLayout);
+ }
+ }
+ }
+
+ public void initForMode(final ActionBar.ContextMode mode) {
+ final ActionBarImpl.ContextMode implMode = (ActionBarImpl.ContextMode) mode;
+
+ if (mCloseButton == null) {
+ mCloseButton = new ImageButton(getContext());
+ mCloseButton.setImageDrawable(mCloseDrawable);
+ mCloseButton.setBackgroundDrawable(null);
+ mCloseButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mode.finish();
+ }
+ });
+ }
+ addView(mCloseButton);
+
+ final Context context = getContext();
+ final Menu menu = mode.getMenu();
+ final int itemCount = menu.size();
+ for (int i = 0; i < itemCount; i++) {
+ final MenuItem item = menu.getItem(i);
+ final ImageButton button = new ImageButton(context, null,
+ com.android.internal.R.attr.actionButtonStyle);
+ button.setClickable(true);
+ button.setFocusable(true);
+ button.setImageDrawable(item.getIcon());
+ button.setId(item.getItemId());
+ button.setVisibility(item.isVisible() ? VISIBLE : GONE);
+ button.setEnabled(item.isEnabled());
+
+ button.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ implMode.dispatchOnContextItemClicked(item);
+ }
+ });
+
+ addView(button);
+ }
+ requestLayout();
+ }
+
+ public void closeMode() {
+ removeAllViews();
+ mCustomView = null;
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ // Used by custom views if they don't supply layout params. Everything else
+ // added to an ActionBarContextView should have them already.
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_width=\"match_parent\" (or fill_parent)");
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode != MeasureSpec.AT_MOST) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_height=\"wrap_content\"");
+ }
+
+ final int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int itemMargin = mItemPadding;
+
+ int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
+ final int height = mContentHeight - getPaddingTop() - getPaddingBottom();
+ final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+
+ if (mCloseButton != null) {
+ availableWidth = measureChildView(mCloseButton, availableWidth,
+ childSpecHeight, itemMargin);
+ }
+
+ if (mTitleLayout != null && mCustomView == null) {
+ availableWidth = measureChildView(mTitleLayout, availableWidth,
+ childSpecHeight, itemMargin);
+ }
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child == mCloseButton || child == mTitleLayout || child == mCustomView) {
+ continue;
+ }
+
+ availableWidth = measureChildView(child, availableWidth, childSpecHeight, itemMargin);
+ }
+
+ if (mCustomView != null) {
+ LayoutParams lp = mCustomView.getLayoutParams();
+ final int customWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int customWidth = lp.width >= 0 ?
+ Math.min(lp.width, availableWidth) : availableWidth;
+ final int customHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int customHeight = lp.height >= 0 ?
+ Math.min(lp.height, height) : height;
+ mCustomView.measure(MeasureSpec.makeMeasureSpec(customWidth, customWidthMode),
+ MeasureSpec.makeMeasureSpec(customHeight, customHeightMode));
+ }
+
+ setMeasuredDimension(contentWidth, mContentHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int x = getPaddingLeft();
+ final int y = getPaddingTop();
+ final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
+ final int itemMargin = mItemPadding;
+
+ if (mCloseButton != null && mCloseButton.getVisibility() != GONE) {
+ x += positionChild(mCloseButton, x, y, contentHeight);
+ }
+
+ if (mTitleLayout != null && mCustomView == null) {
+ x += positionChild(mTitleLayout, x, y, contentHeight) + itemMargin;
+ }
+
+ if (mCustomView != null) {
+ x += positionChild(mCustomView, x, y, contentHeight) + itemMargin;
+ }
+
+ x = r - l - getPaddingRight();
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child == mCloseButton || child == mTitleLayout || child == mCustomView) {
+ continue;
+ }
+
+ x -= positionChildInverse(child, x, y, contentHeight) + itemMargin;
+ }
+ }
+
+ private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) {
+ child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ childSpecHeight);
+
+ availableWidth -= child.getMeasuredWidth();
+ availableWidth -= spacing;
+
+ return availableWidth;
+ }
+
+ private int positionChild(View child, int x, int y, int contentHeight) {
+ int childWidth = child.getMeasuredWidth();
+ int childHeight = child.getMeasuredHeight();
+ int childTop = y + (contentHeight - childHeight) / 2;
+
+ child.layout(x, childTop, x + childWidth, childTop + childHeight);
+
+ return childWidth;
+ }
+
+ private int positionChildInverse(View child, int x, int y, int contentHeight) {
+ int childWidth = child.getMeasuredWidth();
+ int childHeight = child.getMeasuredHeight();
+ int childTop = y + (contentHeight - childHeight) / 2;
+
+ child.layout(x - childWidth, childTop, x, childTop + childHeight);
+
+ return childWidth;
+ }
+}
diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java
new file mode 100644
index 0000000..fbff8ae
--- /dev/null
+++ b/core/java/com/android/internal/widget/ActionBarView.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ActionMenuItem;
+import com.android.internal.view.menu.ActionMenuView;
+import com.android.internal.view.menu.MenuBuilder;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.ActionBar.NavigationCallback;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+
+/**
+ * @hide
+ */
+public class ActionBarView extends ViewGroup {
+ private static final String TAG = "ActionBarView";
+
+ // TODO: This must be defined in the default theme
+ private static final int CONTENT_HEIGHT_DIP = 50;
+ private static final int CONTENT_PADDING_DIP = 3;
+ private static final int CONTENT_SPACING_DIP = 6;
+ private static final int CONTENT_ACTION_SPACING_DIP = 12;
+
+ /**
+ * Display options applied by default
+ */
+ public static final int DISPLAY_DEFAULT = 0;
+
+ /**
+ * Display options that require re-layout as opposed to a simple invalidate
+ */
+ private static final int DISPLAY_RELAYOUT_MASK =
+ ActionBar.DISPLAY_HIDE_HOME |
+ ActionBar.DISPLAY_USE_LOGO;
+
+ private final int mContentHeight;
+
+ private int mNavigationMode;
+ private int mDisplayOptions;
+ private int mSpacing;
+ private int mActionSpacing;
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+ private Drawable mIcon;
+ private Drawable mLogo;
+
+ private ImageView mIconView;
+ private ImageView mLogoView;
+ private LinearLayout mTitleLayout;
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private Spinner mSpinner;
+ private LinearLayout mTabLayout;
+ private View mCustomNavView;
+
+ private boolean mShowMenu;
+ private boolean mUserTitle;
+
+ private MenuBuilder mOptionsMenu;
+ private ActionMenuView mMenuView;
+
+ private ActionMenuItem mLogoNavItem;
+
+ private NavigationCallback mCallback;
+
+ private final AdapterView.OnItemSelectedListener mNavItemSelectedListener =
+ new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView parent, View view, int position, long id) {
+ if (mCallback != null) {
+ mCallback.onNavigationItemSelected(position, id);
+ }
+ }
+ public void onNothingSelected(AdapterView parent) {
+ // Do nothing
+ }
+ };
+
+ private OnClickListener mHomeClickListener = null;
+
+ public ActionBarView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ mContentHeight = (int) (CONTENT_HEIGHT_DIP * metrics.density + 0.5f);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar);
+
+ final int colorFilter = a.getColor(R.styleable.ActionBar_colorFilter, 0);
+
+ if (colorFilter != 0) {
+ final Drawable d = getBackground();
+ d.setDither(true);
+ d.setColorFilter(new PorterDuffColorFilter(colorFilter, PorterDuff.Mode.OVERLAY));
+ }
+
+ ApplicationInfo info = context.getApplicationInfo();
+ PackageManager pm = context.getPackageManager();
+ mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode,
+ ActionBar.NAVIGATION_MODE_STANDARD);
+ mTitle = a.getText(R.styleable.ActionBar_title);
+ mSubtitle = a.getText(R.styleable.ActionBar_subtitle);
+ mDisplayOptions = a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT);
+
+ mLogo = a.getDrawable(R.styleable.ActionBar_logo);
+ if (mLogo == null) {
+ mLogo = info.loadLogo(pm);
+ }
+ mIcon = a.getDrawable(R.styleable.ActionBar_icon);
+ if (mIcon == null) {
+ mIcon = info.loadIcon(pm);
+ }
+
+ Drawable background = a.getDrawable(R.styleable.ActionBar_background);
+ if (background != null) {
+ setBackgroundDrawable(background);
+ }
+
+ final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0);
+ if (customNavId != 0) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ mCustomNavView = (View) inflater.inflate(customNavId, null);
+ mNavigationMode = ActionBar.NAVIGATION_MODE_CUSTOM;
+ }
+
+ a.recycle();
+
+ // TODO: Set this in the theme
+ int padding = (int) (CONTENT_PADDING_DIP * metrics.density + 0.5f);
+ setPadding(padding, padding, padding, padding);
+
+ mSpacing = (int) (CONTENT_SPACING_DIP * metrics.density + 0.5f);
+ mActionSpacing = (int) (CONTENT_ACTION_SPACING_DIP * metrics.density + 0.5f);
+
+ if (mLogo != null || mIcon != null || mTitle != null) {
+ mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle);
+ mHomeClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ Context context = getContext();
+ if (context instanceof Activity) {
+ Activity activity = (Activity) context;
+ activity.onOptionsItemSelected(mLogoNavItem);
+ }
+ }
+ };
+ }
+ }
+
+ public void setCallback(NavigationCallback callback) {
+ mCallback = callback;
+ }
+
+ public void setMenu(Menu menu) {
+ MenuBuilder builder = (MenuBuilder) menu;
+ mOptionsMenu = builder;
+ if (mMenuView != null) {
+ removeView(mMenuView);
+ }
+ final ActionMenuView menuView = (ActionMenuView) builder.getMenuView(
+ MenuBuilder.TYPE_ACTION_BUTTON, null);
+ mActionSpacing = menuView.getItemMargin();
+ final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT);
+ menuView.setLayoutParams(layoutParams);
+ addView(menuView);
+ mMenuView = menuView;
+ }
+
+ public void setCustomNavigationView(View view) {
+ mCustomNavView = view;
+ if (view != null) {
+ setNavigationMode(ActionBar.NAVIGATION_MODE_CUSTOM);
+ }
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Set the action bar title. This will always replace or override window titles.
+ * @param title Title to set
+ *
+ * @see #setWindowTitle(CharSequence)
+ */
+ public void setTitle(CharSequence title) {
+ mUserTitle = true;
+ setTitleImpl(title);
+ }
+
+ /**
+ * Set the window title. A window title will always be replaced or overridden by a user title.
+ * @param title Title to set
+ *
+ * @see #setTitle(CharSequence)
+ */
+ public void setWindowTitle(CharSequence title) {
+ if (!mUserTitle) {
+ setTitleImpl(title);
+ }
+ }
+
+ private void setTitleImpl(CharSequence title) {
+ mTitle = title;
+ if (mTitleView != null) {
+ mTitleView.setText(title);
+ }
+ if (mLogoNavItem != null) {
+ mLogoNavItem.setTitle(title);
+ }
+ }
+
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ if (mSubtitleView != null) {
+ mSubtitleView.setText(subtitle);
+ }
+ }
+
+ public void setDisplayOptions(int options) {
+ final int flagsChanged = options ^ mDisplayOptions;
+ mDisplayOptions = options;
+ if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) {
+ final int vis = (options & ActionBar.DISPLAY_HIDE_HOME) != 0 ? GONE : VISIBLE;
+ if (mLogoView != null) {
+ mLogoView.setVisibility(vis);
+ }
+ if (mIconView != null) {
+ mIconView.setVisibility(vis);
+ }
+
+ requestLayout();
+ } else {
+ invalidate();
+ }
+ }
+
+ public void setNavigationMode(int mode) {
+ final int oldMode = mNavigationMode;
+ if (mode != oldMode) {
+ switch (oldMode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ if (mTitleLayout != null) {
+ removeView(mTitleLayout);
+ mTitleLayout = null;
+ mTitleView = null;
+ mSubtitleView = null;
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST:
+ if (mSpinner != null) {
+ removeView(mSpinner);
+ mSpinner = null;
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_CUSTOM:
+ if (mCustomNavView != null) {
+ removeView(mCustomNavView);
+ mCustomNavView = null;
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabLayout != null) {
+ removeView(mTabLayout);
+ mTabLayout = null;
+ }
+ }
+
+ switch (mode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ initTitle();
+ break;
+ case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST:
+ mSpinner = new Spinner(mContext, null,
+ com.android.internal.R.attr.dropDownSpinnerStyle);
+ mSpinner.setOnItemSelectedListener(mNavItemSelectedListener);
+ addView(mSpinner);
+ break;
+ case ActionBar.NAVIGATION_MODE_CUSTOM:
+ addView(mCustomNavView);
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ mTabLayout = new LinearLayout(getContext());
+ addView(mTabLayout);
+ break;
+ }
+ mNavigationMode = mode;
+ requestLayout();
+ }
+ }
+
+ public void setDropdownAdapter(SpinnerAdapter adapter) {
+ mSpinner.setAdapter(adapter);
+ }
+
+ public View getCustomNavigationView() {
+ return mCustomNavView;
+ }
+
+ public int getNavigationMode() {
+ return mNavigationMode;
+ }
+
+ public int getDisplayOptions() {
+ return mDisplayOptions;
+ }
+
+ private TabView createTabView(ActionBar.Tab tab) {
+ final TabView tabView = new TabView(getContext(), tab);
+ tabView.setFocusable(true);
+ tabView.setOnClickListener(new TabClickListener());
+ return tabView;
+ }
+
+ public void addTab(ActionBar.Tab tab) {
+ final boolean isFirst = mTabLayout.getChildCount() == 0;
+ final TabView tabView = createTabView(tab);
+ mTabLayout.addView(tabView);
+ if (isFirst) {
+ tabView.setSelected(true);
+ }
+ }
+
+ public void insertTab(ActionBar.Tab tab, int position) {
+ final boolean isFirst = mTabLayout.getChildCount() == 0;
+ final TabView tabView = createTabView(tab);
+ mTabLayout.addView(tabView, position);
+ if (isFirst) {
+ tabView.setSelected(true);
+ }
+ }
+
+ public void removeTabAt(int position) {
+ mTabLayout.removeViewAt(position);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ // Used by custom nav views if they don't supply layout params. Everything else
+ // added to an ActionBarView should have them already.
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ if ((mDisplayOptions & ActionBar.DISPLAY_HIDE_HOME) == 0) {
+ if (mLogo != null && (mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) {
+ mLogoView = new ImageView(getContext());
+ mLogoView.setAdjustViewBounds(true);
+ mLogoView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT));
+ mLogoView.setImageDrawable(mLogo);
+ mLogoView.setClickable(true);
+ mLogoView.setFocusable(true);
+ mLogoView.setOnClickListener(mHomeClickListener);
+ addView(mLogoView);
+ } else if (mIcon != null) {
+ mIconView = new ImageView(getContext());
+ mIconView.setAdjustViewBounds(true);
+ mIconView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT));
+ mIconView.setImageDrawable(mIcon);
+ mIconView.setClickable(true);
+ mIconView.setFocusable(true);
+ mIconView.setOnClickListener(mHomeClickListener);
+ addView(mIconView);
+ }
+ }
+
+ switch (mNavigationMode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ if (mLogoView == null) {
+ initTitle();
+ }
+ break;
+
+ case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST:
+ throw new UnsupportedOperationException(
+ "Inflating dropdown list navigation isn't supported yet!");
+
+ case ActionBar.NAVIGATION_MODE_TABS:
+ throw new UnsupportedOperationException(
+ "Inflating tab navigation isn't supported yet!");
+
+ case ActionBar.NAVIGATION_MODE_CUSTOM:
+ if (mCustomNavView != null) {
+ addView(mCustomNavView);
+ }
+ break;
+ }
+ }
+
+ private void initTitle() {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ mTitleLayout = (LinearLayout) inflater.inflate(R.layout.action_bar_title_item, null);
+ mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title);
+ mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle);
+ if (mTitle != null) {
+ mTitleView.setText(mTitle);
+ }
+ if (mSubtitle != null) {
+ mSubtitleView.setText(mSubtitle);
+ mSubtitleView.setVisibility(VISIBLE);
+ }
+ addView(mTitleLayout);
+ }
+
+ public void setTabSelected(int position) {
+ final int tabCount = mTabLayout.getChildCount();
+ for (int i = 0; i < tabCount; i++) {
+ final View child = mTabLayout.getChildAt(i);
+ child.setSelected(i == position);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_width=\"match_parent\" (or fill_parent)");
+ }
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode != MeasureSpec.AT_MOST) {
+ throw new IllegalStateException(getClass().getSimpleName() + " can only be used " +
+ "with android:layout_height=\"wrap_content\"");
+ }
+
+ int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
+
+ int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
+ final int height = mContentHeight - getPaddingTop() - getPaddingBottom();
+ final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+
+ if (mLogoView != null && mLogoView.getVisibility() != GONE) {
+ availableWidth = measureChildView(mLogoView, availableWidth, childSpecHeight, mSpacing);
+ }
+ if (mIconView != null && mIconView.getVisibility() != GONE) {
+ availableWidth = measureChildView(mIconView, availableWidth, childSpecHeight, mSpacing);
+ }
+
+ if (mMenuView != null) {
+ availableWidth = measureChildView(mMenuView, availableWidth,
+ childSpecHeight, 0);
+ }
+
+ switch (mNavigationMode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ if (mTitleLayout != null) {
+ measureChildView(mTitleLayout, availableWidth, childSpecHeight, mSpacing);
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST:
+ if (mSpinner != null) {
+ mSpinner.measure(
+ MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_CUSTOM:
+ if (mCustomNavView != null) {
+ LayoutParams lp = mCustomNavView.getLayoutParams();
+ final int customNavWidthMode = lp.width != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int customNavWidth = lp.width >= 0 ?
+ Math.min(lp.width, availableWidth) : availableWidth;
+ final int customNavHeightMode = lp.height != LayoutParams.WRAP_CONTENT ?
+ MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int customNavHeight = lp.height >= 0 ?
+ Math.min(lp.height, height) : height;
+ mCustomNavView.measure(
+ MeasureSpec.makeMeasureSpec(customNavWidth, customNavWidthMode),
+ MeasureSpec.makeMeasureSpec(customNavHeight, customNavHeightMode));
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabLayout != null) {
+ mTabLayout.measure(
+ MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ }
+ break;
+ }
+
+ setMeasuredDimension(contentWidth, mContentHeight);
+ }
+
+ private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) {
+ child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ childSpecHeight);
+
+ availableWidth -= child.getMeasuredWidth();
+ availableWidth -= spacing;
+
+ return availableWidth;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int x = getPaddingLeft();
+ final int y = getPaddingTop();
+ final int contentHeight = b - t - getPaddingTop() - getPaddingBottom();
+
+ if (mLogoView != null && mLogoView.getVisibility() != GONE) {
+ x += positionChild(mLogoView, x, y, contentHeight) + mSpacing;
+ }
+ if (mIconView != null && mIconView.getVisibility() != GONE) {
+ x += positionChild(mIconView, x, y, contentHeight) + mSpacing;
+ }
+
+ switch (mNavigationMode) {
+ case ActionBar.NAVIGATION_MODE_STANDARD:
+ if (mTitleLayout != null) {
+ x += positionChild(mTitleLayout, x, y, contentHeight) + mSpacing;
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_DROPDOWN_LIST:
+ if (mSpinner != null) {
+ x += positionChild(mSpinner, x, y, contentHeight) + mSpacing;
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_CUSTOM:
+ if (mCustomNavView != null) {
+ x += positionChild(mCustomNavView, x, y, contentHeight) + mSpacing;
+ }
+ break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabLayout != null) {
+ x += positionChild(mTabLayout, x, y, contentHeight) + mSpacing;
+ }
+ }
+
+ x = r - l - getPaddingRight();
+
+ if (mMenuView != null) {
+ x -= positionChildInverse(mMenuView, x + mActionSpacing, y, contentHeight)
+ - mActionSpacing;
+ }
+ }
+
+ private int positionChild(View child, int x, int y, int contentHeight) {
+ int childWidth = child.getMeasuredWidth();
+ int childHeight = child.getMeasuredHeight();
+ int childTop = y + (contentHeight - childHeight) / 2;
+
+ child.layout(x, childTop, x + childWidth, childTop + childHeight);
+
+ return childWidth;
+ }
+
+ private int positionChildInverse(View child, int x, int y, int contentHeight) {
+ int childWidth = child.getMeasuredWidth();
+ int childHeight = child.getMeasuredHeight();
+ int childTop = y + (contentHeight - childHeight) / 2;
+
+ child.layout(x - childWidth, childTop, x, childTop + childHeight);
+
+ return childWidth;
+ }
+
+ private static class TabView extends LinearLayout {
+ private ActionBar.Tab mTab;
+
+ public TabView(Context context, ActionBar.Tab tab) {
+ super(context);
+ mTab = tab;
+
+ // TODO Style tabs based on the theme
+
+ final Drawable icon = tab.getIcon();
+ final CharSequence text = tab.getText();
+
+ if (icon != null) {
+ ImageView iconView = new ImageView(context);
+ iconView.setImageDrawable(icon);
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_VERTICAL;
+ iconView.setLayoutParams(lp);
+ addView(iconView);
+ }
+
+ if (text != null) {
+ TextView textView = new TextView(context);
+ textView.setText(text);
+ textView.setSingleLine();
+ textView.setEllipsize(TruncateAt.END);
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_VERTICAL;
+ textView.setLayoutParams(lp);
+ addView(textView);
+ }
+
+ setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT, 1));
+ }
+
+ public ActionBar.Tab getTab() {
+ return mTab;
+ }
+ }
+
+ private class TabClickListener implements OnClickListener {
+ public void onClick(View view) {
+ TabView tabView = (TabView) view;
+ tabView.getTab().select();
+ final int tabCount = mTabLayout.getChildCount();
+ for (int i = 0; i < tabCount; i++) {
+ final View child = mTabLayout.getChildAt(i);
+ child.setSelected(child == view);
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/ContactHeaderWidget.java b/core/java/com/android/internal/widget/ContactHeaderWidget.java
deleted file mode 100644
index f421466..0000000
--- a/core/java/com/android/internal/widget/ContactHeaderWidget.java
+++ /dev/null
@@ -1,661 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.widget;
-
-import com.android.internal.R;
-
-import android.Manifest;
-import android.content.AsyncQueryHandler;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.SystemClock;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.PhoneLookup;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.StatusUpdates;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.QuickContactBadge;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * Header used across system for displaying a title bar with contact info. You
- * can bind specific values on the header, or use helper methods like
- * {@link #bindFromContactId(long)} to populate asynchronously.
- * <p>
- * The parent must request the {@link Manifest.permission#READ_CONTACTS}
- * permission to access contact data.
- */
-public class ContactHeaderWidget extends FrameLayout implements View.OnClickListener {
-
- private static final String TAG = "ContactHeaderWidget";
-
- private TextView mDisplayNameView;
- private View mAggregateBadge;
- private TextView mPhoneticNameView;
- private CheckBox mStarredView;
- private QuickContactBadge mPhotoView;
- private ImageView mPresenceView;
- private TextView mStatusView;
- private TextView mStatusAttributionView;
- private int mNoPhotoResource;
- private QueryHandler mQueryHandler;
-
- protected Uri mContactUri;
-
- protected String[] mExcludeMimes = null;
-
- protected ContentResolver mContentResolver;
-
- /**
- * Interface for callbacks invoked when the user interacts with a header.
- */
- public interface ContactHeaderListener {
- public void onPhotoClick(View view);
- public void onDisplayNameClick(View view);
- }
-
- private ContactHeaderListener mListener;
-
-
- private interface ContactQuery {
- //Projection used for the summary info in the header.
- String[] COLUMNS = new String[] {
- Contacts._ID,
- Contacts.LOOKUP_KEY,
- Contacts.PHOTO_ID,
- Contacts.DISPLAY_NAME,
- Contacts.PHONETIC_NAME,
- Contacts.STARRED,
- Contacts.CONTACT_PRESENCE,
- Contacts.CONTACT_STATUS,
- Contacts.CONTACT_STATUS_TIMESTAMP,
- Contacts.CONTACT_STATUS_RES_PACKAGE,
- Contacts.CONTACT_STATUS_LABEL,
- };
- int _ID = 0;
- int LOOKUP_KEY = 1;
- int PHOTO_ID = 2;
- int DISPLAY_NAME = 3;
- int PHONETIC_NAME = 4;
- //TODO: We need to figure out how we're going to get the phonetic name.
- //static final int HEADER_PHONETIC_NAME_COLUMN_INDEX
- int STARRED = 5;
- int CONTACT_PRESENCE_STATUS = 6;
- int CONTACT_STATUS = 7;
- int CONTACT_STATUS_TIMESTAMP = 8;
- int CONTACT_STATUS_RES_PACKAGE = 9;
- int CONTACT_STATUS_LABEL = 10;
- }
-
- private interface PhotoQuery {
- String[] COLUMNS = new String[] {
- Photo.PHOTO
- };
-
- int PHOTO = 0;
- }
-
- //Projection used for looking up contact id from phone number
- protected static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
- PhoneLookup._ID,
- PhoneLookup.LOOKUP_KEY,
- };
- protected static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
- protected static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
-
- //Projection used for looking up contact id from email address
- protected static final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
- RawContacts.CONTACT_ID,
- Contacts.LOOKUP_KEY,
- };
- protected static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
- protected static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
-
- protected static final String[] CONTACT_LOOKUP_PROJECTION = new String[] {
- Contacts._ID,
- };
- protected static final int CONTACT_LOOKUP_ID_COLUMN_INDEX = 0;
-
- private static final int TOKEN_CONTACT_INFO = 0;
- private static final int TOKEN_PHONE_LOOKUP = 1;
- private static final int TOKEN_EMAIL_LOOKUP = 2;
- private static final int TOKEN_PHOTO_QUERY = 3;
-
- public ContactHeaderWidget(Context context) {
- this(context, null);
- }
-
- public ContactHeaderWidget(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ContactHeaderWidget(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- mContentResolver = mContext.getContentResolver();
-
- LayoutInflater inflater =
- (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- inflater.inflate(R.layout.contact_header, this);
-
- mDisplayNameView = (TextView) findViewById(R.id.name);
- mAggregateBadge = findViewById(R.id.aggregate_badge);
-
- mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
-
- mStarredView = (CheckBox)findViewById(R.id.star);
- mStarredView.setOnClickListener(this);
-
- mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
-
- mPresenceView = (ImageView) findViewById(R.id.presence);
-
- mStatusView = (TextView)findViewById(R.id.status);
- mStatusAttributionView = (TextView)findViewById(R.id.status_date);
-
- // Set the photo with a random "no contact" image
- long now = SystemClock.elapsedRealtime();
- int num = (int) now & 0xf;
- if (num < 9) {
- // Leaning in from right, common
- mNoPhotoResource = R.drawable.ic_contact_picture;
- } else if (num < 14) {
- // Leaning in from left uncommon
- mNoPhotoResource = R.drawable.ic_contact_picture_2;
- } else {
- // Coming in from the top, rare
- mNoPhotoResource = R.drawable.ic_contact_picture_3;
- }
-
- resetAsyncQueryHandler();
- }
-
- public void enableClickListeners() {
- mDisplayNameView.setOnClickListener(this);
- mPhotoView.setOnClickListener(this);
- }
-
- /**
- * Set the given {@link ContactHeaderListener} to handle header events.
- */
- public void setContactHeaderListener(ContactHeaderListener listener) {
- mListener = listener;
- }
-
- private void performPhotoClick() {
- if (mListener != null) {
- mListener.onPhotoClick(mPhotoView);
- }
- }
-
- private void performDisplayNameClick() {
- if (mListener != null) {
- mListener.onDisplayNameClick(mDisplayNameView);
- }
- }
-
- private class QueryHandler extends AsyncQueryHandler {
-
- public QueryHandler(ContentResolver cr) {
- super(cr);
- }
-
- @Override
- protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
- try{
- if (this != mQueryHandler) {
- Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!");
- return;
- }
-
- switch (token) {
- case TOKEN_PHOTO_QUERY: {
- //Set the photo
- Bitmap photoBitmap = null;
- if (cursor != null && cursor.moveToFirst()
- && !cursor.isNull(PhotoQuery.PHOTO)) {
- byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO);
- photoBitmap = BitmapFactory.decodeByteArray(photoData, 0,
- photoData.length, null);
- }
-
- if (photoBitmap == null) {
- photoBitmap = loadPlaceholderPhoto(null);
- }
- mPhotoView.setImageBitmap(photoBitmap);
- if (cookie != null && cookie instanceof Uri) {
- mPhotoView.assignContactUri((Uri) cookie);
- }
- invalidate();
- break;
- }
- case TOKEN_CONTACT_INFO: {
- if (cursor != null && cursor.moveToFirst()) {
- bindContactInfo(cursor);
- Uri lookupUri = Contacts.getLookupUri(cursor.getLong(ContactQuery._ID),
- cursor.getString(ContactQuery.LOOKUP_KEY));
-
- final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
-
- if (photoId == 0) {
- mPhotoView.setImageBitmap(loadPlaceholderPhoto(null));
- if (cookie != null && cookie instanceof Uri) {
- mPhotoView.assignContactUri((Uri) cookie);
- }
- invalidate();
- } else {
- startPhotoQuery(photoId, lookupUri,
- false /* don't reset query handler */);
- }
- } else {
- // shouldn't really happen
- setDisplayName(null, null);
- setSocialSnippet(null);
- setPhoto(loadPlaceholderPhoto(null));
- }
- break;
- }
- case TOKEN_PHONE_LOOKUP: {
- if (cursor != null && cursor.moveToFirst()) {
- long contactId = cursor.getLong(PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX);
- String lookupKey = cursor.getString(
- PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX);
- bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey),
- false /* don't reset query handler */);
- } else {
- String phoneNumber = (String) cookie;
- setDisplayName(phoneNumber, null);
- setSocialSnippet(null);
- setPhoto(loadPlaceholderPhoto(null));
- mPhotoView.assignContactFromPhone(phoneNumber, true);
- }
- break;
- }
- case TOKEN_EMAIL_LOOKUP: {
- if (cursor != null && cursor.moveToFirst()) {
- long contactId = cursor.getLong(EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX);
- String lookupKey = cursor.getString(
- EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX);
- bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey),
- false /* don't reset query handler */);
- } else {
- String emailAddress = (String) cookie;
- setDisplayName(emailAddress, null);
- setSocialSnippet(null);
- setPhoto(loadPlaceholderPhoto(null));
- mPhotoView.assignContactFromEmail(emailAddress, true);
- }
- break;
- }
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- }
-
- /**
- * Turn on/off showing of the aggregate badge element.
- */
- public void showAggregateBadge(boolean showBagde) {
- mAggregateBadge.setVisibility(showBagde ? View.VISIBLE : View.GONE);
- }
-
- /**
- * Turn on/off showing of the star element.
- */
- public void showStar(boolean showStar) {
- mStarredView.setVisibility(showStar ? View.VISIBLE : View.GONE);
- }
-
- /**
- * Manually set the starred state of this header widget. This doesn't change
- * the underlying {@link Contacts} value, only the UI state.
- */
- public void setStared(boolean starred) {
- mStarredView.setChecked(starred);
- }
-
- /**
- * Manually set the presence.
- */
- public void setPresence(int presence) {
- mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence));
- }
-
- /**
- * Manually set the contact uri
- */
- public void setContactUri(Uri uri) {
- setContactUri(uri, true);
- }
-
- /**
- * Manually set the contact uri
- */
- public void setContactUri(Uri uri, boolean sendToFastrack) {
- mContactUri = uri;
- if (sendToFastrack) {
- mPhotoView.assignContactUri(uri);
- }
- }
-
- /**
- * Manually set the photo to display in the header. This doesn't change the
- * underlying {@link Contacts}, only the UI state.
- */
- public void setPhoto(Bitmap bitmap) {
- mPhotoView.setImageBitmap(bitmap);
- }
-
- /**
- * Manually set the display name and phonetic name to show in the header.
- * This doesn't change the underlying {@link Contacts}, only the UI state.
- */
- public void setDisplayName(CharSequence displayName, CharSequence phoneticName) {
- mDisplayNameView.setText(displayName);
- if (!TextUtils.isEmpty(phoneticName)) {
- mPhoneticNameView.setText(phoneticName);
- mPhoneticNameView.setVisibility(View.VISIBLE);
- } else {
- mPhoneticNameView.setVisibility(View.GONE);
- }
- }
-
- /**
- * Manually set the social snippet text to display in the header.
- */
- public void setSocialSnippet(CharSequence snippet) {
- if (snippet == null) {
- mStatusView.setVisibility(View.GONE);
- mStatusAttributionView.setVisibility(View.GONE);
- } else {
- mStatusView.setText(snippet);
- mStatusView.setVisibility(View.VISIBLE);
- }
- }
-
- /**
- * Set a list of specific MIME-types to exclude and not display. For
- * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
- * profile icon.
- */
- public void setExcludeMimes(String[] excludeMimes) {
- mExcludeMimes = excludeMimes;
- mPhotoView.setExcludeMimes(excludeMimes);
- }
-
- /**
- * Convenience method for binding all available data from an existing
- * contact.
- *
- * @param contactLookupUri a {Contacts.CONTENT_LOOKUP_URI} style URI.
- */
- public void bindFromContactLookupUri(Uri contactLookupUri) {
- bindFromContactUriInternal(contactLookupUri, true /* reset query handler */);
- }
-
- /**
- * Convenience method for binding all available data from an existing
- * contact.
- *
- * @param contactUri a {Contacts.CONTENT_URI} style URI.
- * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
- */
- private void bindFromContactUriInternal(Uri contactUri, boolean resetQueryHandler) {
- mContactUri = contactUri;
- startContactQuery(contactUri, resetQueryHandler);
- }
-
- /**
- * Convenience method for binding all available data from an existing
- * contact.
- *
- * @param emailAddress The email address used to do a reverse lookup in
- * the contacts database. If more than one contact contains this email
- * address, one of them will be chosen to bind to.
- */
- public void bindFromEmail(String emailAddress) {
- resetAsyncQueryHandler();
-
- mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, emailAddress,
- Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
- EMAIL_LOOKUP_PROJECTION, null, null, null);
- }
-
- /**
- * Convenience method for binding all available data from an existing
- * contact.
- *
- * @param number The phone number used to do a reverse lookup in
- * the contacts database. If more than one contact contains this phone
- * number, one of them will be chosen to bind to.
- */
- public void bindFromPhoneNumber(String number) {
- resetAsyncQueryHandler();
-
- mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, number,
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
- PHONE_LOOKUP_PROJECTION, null, null, null);
- }
-
- /**
- * startContactQuery
- *
- * internal method to query contact by Uri.
- *
- * @param contactUri the contact uri
- * @param resetQueryHandler whether to use a new AsyncQueryHandler or not
- */
- private void startContactQuery(Uri contactUri, boolean resetQueryHandler) {
- if (resetQueryHandler) {
- resetAsyncQueryHandler();
- }
-
- mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS,
- null, null, null);
- }
-
- /**
- * startPhotoQuery
- *
- * internal method to query contact photo by photo id and uri.
- *
- * @param photoId the photo id.
- * @param lookupKey the lookup uri.
- * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
- */
- protected void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) {
- if (resetQueryHandler) {
- resetAsyncQueryHandler();
- }
-
- mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey,
- ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PhotoQuery.COLUMNS,
- null, null, null);
- }
-
- /**
- * Method to force this widget to forget everything it knows about the contact.
- * We need to stop any existing async queries for phone, email, contact, and photos.
- */
- public void wipeClean() {
- resetAsyncQueryHandler();
-
- setDisplayName(null, null);
- setPhoto(loadPlaceholderPhoto(null));
- setSocialSnippet(null);
- setPresence(0);
- mContactUri = null;
- mExcludeMimes = null;
- }
-
-
- private void resetAsyncQueryHandler() {
- // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really
- // need the old async queries to be cancelled, let's do it the hard way.
- mQueryHandler = new QueryHandler(mContentResolver);
- }
-
- /**
- * Bind the contact details provided by the given {@link Cursor}.
- */
- protected void bindContactInfo(Cursor c) {
- final String displayName = c.getString(ContactQuery.DISPLAY_NAME);
- final String phoneticName = c.getString(ContactQuery.PHONETIC_NAME);
- this.setDisplayName(displayName, phoneticName);
-
- final boolean starred = c.getInt(ContactQuery.STARRED) != 0;
- mStarredView.setChecked(starred);
-
- //Set the presence status
- if (!c.isNull(ContactQuery.CONTACT_PRESENCE_STATUS)) {
- int presence = c.getInt(ContactQuery.CONTACT_PRESENCE_STATUS);
- mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence));
- mPresenceView.setVisibility(View.VISIBLE);
- } else {
- mPresenceView.setVisibility(View.GONE);
- }
-
- //Set the status update
- String status = c.getString(ContactQuery.CONTACT_STATUS);
- if (!TextUtils.isEmpty(status)) {
- mStatusView.setText(status);
- mStatusView.setVisibility(View.VISIBLE);
-
- CharSequence timestamp = null;
-
- if (!c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)) {
- long date = c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
-
- // Set the date/time field by mixing relative and absolute
- // times.
- int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
-
- timestamp = DateUtils.getRelativeTimeSpanString(date,
- System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags);
- }
-
- String label = null;
-
- if (!c.isNull(ContactQuery.CONTACT_STATUS_LABEL)) {
- String resPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE);
- int labelResource = c.getInt(ContactQuery.CONTACT_STATUS_LABEL);
- Resources resources;
- if (TextUtils.isEmpty(resPackage)) {
- resources = getResources();
- } else {
- PackageManager pm = getContext().getPackageManager();
- try {
- resources = pm.getResourcesForApplication(resPackage);
- } catch (NameNotFoundException e) {
- Log.w(TAG, "Contact status update resource package not found: "
- + resPackage);
- resources = null;
- }
- }
-
- if (resources != null) {
- try {
- label = resources.getString(labelResource);
- } catch (NotFoundException e) {
- Log.w(TAG, "Contact status update resource not found: " + resPackage + "@"
- + labelResource);
- }
- }
- }
-
- CharSequence attribution;
- if (timestamp != null && label != null) {
- attribution = getContext().getString(
- R.string.contact_status_update_attribution_with_date,
- timestamp, label);
- } else if (timestamp == null && label != null) {
- attribution = getContext().getString(
- R.string.contact_status_update_attribution,
- label);
- } else if (timestamp != null) {
- attribution = timestamp;
- } else {
- attribution = null;
- }
- if (attribution != null) {
- mStatusAttributionView.setText(attribution);
- mStatusAttributionView.setVisibility(View.VISIBLE);
- } else {
- mStatusAttributionView.setVisibility(View.GONE);
- }
- } else {
- mStatusView.setVisibility(View.GONE);
- mStatusAttributionView.setVisibility(View.GONE);
- }
- }
-
- public void onClick(View view) {
- switch (view.getId()) {
- case R.id.star: {
- // Toggle "starred" state
- // Make sure there is a contact
- if (mContactUri != null) {
- final ContentValues values = new ContentValues(1);
- values.put(Contacts.STARRED, mStarredView.isChecked());
- mContentResolver.update(mContactUri, values, null, null);
- }
- break;
- }
- case R.id.photo: {
- performPhotoClick();
- break;
- }
- case R.id.name: {
- performDisplayNameClick();
- break;
- }
- }
- }
-
- private Bitmap loadPlaceholderPhoto(BitmapFactory.Options options) {
- if (mNoPhotoResource == 0) {
- return null;
- }
- return BitmapFactory.decodeResource(mContext.getResources(),
- mNoPhotoResource, options);
- }
-}
diff --git a/core/java/com/android/internal/widget/DigitalClock.java b/core/java/com/android/internal/widget/DigitalClock.java
index fa47ff6..23e2277 100644
--- a/core/java/com/android/internal/widget/DigitalClock.java
+++ b/core/java/com/android/internal/widget/DigitalClock.java
@@ -30,7 +30,7 @@ import android.provider.Settings;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
-import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import java.text.DateFormatSymbols;
@@ -39,7 +39,7 @@ import java.util.Calendar;
/**
* Displays the time
*/
-public class DigitalClock extends LinearLayout {
+public class DigitalClock extends RelativeLayout {
private final static String M12 = "h:mm";
private final static String M24 = "kk:mm";
diff --git a/core/java/com/android/internal/widget/EditStyledText.java b/core/java/com/android/internal/widget/EditStyledText.java
deleted file mode 100644
index 82197c0..0000000
--- a/core/java/com/android/internal/widget/EditStyledText.java
+++ /dev/null
@@ -1,1663 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.widget;
-
-import java.io.InputStream;
-import java.util.ArrayList;
-
-import android.app.AlertDialog.Builder;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RectShape;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.Html;
-import android.text.Layout;
-import android.text.Spannable;
-import android.text.Spanned;
-import android.text.method.ArrowKeyMovementMethod;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.AlignmentSpan;
-import android.text.style.CharacterStyle;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.ImageSpan;
-import android.text.style.ParagraphStyle;
-import android.text.style.QuoteSpan;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-import android.widget.TextView;
-
-/**
- * EditStyledText extends EditText for managing the flow and status to edit
- * the styled text. This manages the states and flows of editing, supports
- * inserting image, import/export HTML.
- */
-public class EditStyledText extends EditText {
-
- private static final String LOG_TAG = "EditStyledText";
- private static final boolean DBG = false;
-
- /**
- * The modes of editing actions.
- */
- /** The mode that no editing action is done. */
- public static final int MODE_NOTHING = 0;
- /** The mode of copy. */
- public static final int MODE_COPY = 1;
- /** The mode of paste. */
- public static final int MODE_PASTE = 2;
- /** The mode of changing size. */
- public static final int MODE_SIZE = 3;
- /** The mode of changing color. */
- public static final int MODE_COLOR = 4;
- /** The mode of selection. */
- public static final int MODE_SELECT = 5;
- /** The mode of changing alignment. */
- public static final int MODE_ALIGN = 6;
- /** The mode of changing cut. */
- public static final int MODE_CUT = 7;
-
- /**
- * The state of selection.
- */
- /** The state that selection isn't started. */
- public static final int STATE_SELECT_OFF = 0;
- /** The state that selection is started. */
- public static final int STATE_SELECT_ON = 1;
- /** The state that selection is done, but not fixed. */
- public static final int STATE_SELECTED = 2;
- /** The state that selection is done and not fixed. */
- public static final int STATE_SELECT_FIX = 3;
-
- /**
- * The help message strings.
- */
- public static final int HINT_MSG_NULL = 0;
- public static final int HINT_MSG_COPY_BUF_BLANK = 1;
- public static final int HINT_MSG_SELECT_START = 2;
- public static final int HINT_MSG_SELECT_END = 3;
- public static final int HINT_MSG_PUSH_COMPETE = 4;
-
-
- /**
- * The help message strings.
- */
- public static final int DEFAULT_BACKGROUND_COLOR = 0x00FFFFFF;
-
- /**
- * EditStyledTextInterface provides functions for notifying messages to
- * calling class.
- */
- public interface EditStyledTextNotifier {
- public void notifyHintMsg(int msgId);
- public void notifyStateChanged(int mode, int state);
- }
-
- private EditStyledTextNotifier mESTInterface;
-
- /**
- * EditStyledTextEditorManager manages the flow and status of each
- * function for editing styled text.
- */
- private EditorManager mManager;
- private StyledTextConverter mConverter;
- private StyledTextDialog mDialog;
- private Drawable mDefaultBackground;
- private int mBackgroundColor;
-
- /**
- * EditStyledText extends EditText for managing flow of each editing
- * action.
- */
- public EditStyledText(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init();
- }
-
- public EditStyledText(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- public EditStyledText(Context context) {
- super(context);
- init();
- }
-
- /**
- * Set Notifier.
- */
- public void setNotifier(EditStyledTextNotifier estInterface) {
- mESTInterface = estInterface;
- }
-
- /**
- * Set Builder for AlertDialog.
- *
- * @param builder
- * Builder for opening Alert Dialog.
- */
- public void setBuilder(Builder builder) {
- mDialog.setBuilder(builder);
- }
-
- /**
- * Set Parameters for ColorAlertDialog.
- *
- * @param colortitle
- * Title for Alert Dialog.
- * @param colornames
- * List of name of selecting color.
- * @param colorints
- * List of int of color.
- */
- public void setColorAlertParams(CharSequence colortitle,
- CharSequence[] colornames, CharSequence[] colorints) {
- mDialog.setColorAlertParams(colortitle, colornames, colorints);
- }
-
- /**
- * Set Parameters for SizeAlertDialog.
- *
- * @param sizetitle
- * Title for Alert Dialog.
- * @param sizenames
- * List of name of selecting size.
- * @param sizedisplayints
- * List of int of size displayed in TextView.
- * @param sizesendints
- * List of int of size exported to HTML.
- */
- public void setSizeAlertParams(CharSequence sizetitle,
- CharSequence[] sizenames, CharSequence[] sizedisplayints,
- CharSequence[] sizesendints) {
- mDialog.setSizeAlertParams(sizetitle, sizenames, sizedisplayints,
- sizesendints);
- }
-
- public void setAlignAlertParams(CharSequence aligntitle,
- CharSequence[] alignnames) {
- mDialog.setAlignAlertParams(aligntitle, alignnames);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (mManager.isSoftKeyBlocked() &&
- event.getAction() == MotionEvent.ACTION_UP) {
- cancelLongPress();
- }
- final boolean superResult = super.onTouchEvent(event);
- if (event.getAction() == MotionEvent.ACTION_UP) {
- if (DBG) {
- Log.d(LOG_TAG, "--- onTouchEvent");
- }
- mManager.onCursorMoved();
- }
- return superResult;
- }
-
- /**
- * Start editing. This function have to be called before other editing
- * actions.
- */
- public void onStartEdit() {
- mManager.onStartEdit();
- }
-
- /**
- * End editing.
- */
- public void onEndEdit() {
- mManager.onEndEdit();
- }
-
- /**
- * Start "Copy" action.
- */
- public void onStartCopy() {
- mManager.onStartCopy();
- }
-
- /**
- * Start "Cut" action.
- */
- public void onStartCut() {
- mManager.onStartCut();
- }
-
- /**
- * Start "Paste" action.
- */
- public void onStartPaste() {
- mManager.onStartPaste();
- }
-
- /**
- * Start changing "Size" action.
- */
- public void onStartSize() {
- mManager.onStartSize();
- }
-
- /**
- * Start changing "Color" action.
- */
- public void onStartColor() {
- mManager.onStartColor();
- }
-
- /**
- * Start changing "BackgroundColor" action.
- */
- public void onStartBackgroundColor() {
- mManager.onStartBackgroundColor();
- }
-
- /**
- * Start changing "Alignment" action.
- */
- public void onStartAlign() {
- mManager.onStartAlign();
- }
-
- /**
- * Start "Select" action.
- */
- public void onStartSelect() {
- mManager.onStartSelect();
- }
-
- /**
- * Start "SelectAll" action.
- */
- public void onStartSelectAll() {
- mManager.onStartSelectAll();
- }
-
- /**
- * Fix Selected Item.
- */
- public void onFixSelectedItem() {
- mManager.onFixSelectedItem();
- }
-
- /**
- * InsertImage to TextView by using URI
- *
- * @param uri
- * URI of the iamge inserted to TextView.
- */
- public void onInsertImage(Uri uri) {
- mManager.onInsertImage(uri);
- }
-
- /**
- * InsertImage to TextView by using resource ID
- *
- * @param resId
- * Resource ID of the iamge inserted to TextView.
- */
- public void onInsertImage(int resId) {
- mManager.onInsertImage(resId);
- }
-
- public void onInsertHorizontalLine() {
- mManager.onInsertHorizontalLine();
- }
-
- public void onClearStyles() {
- mManager.onClearStyles();
- }
- /**
- * Set Size of the Item.
- *
- * @param size
- * The size of the Item.
- */
- public void setItemSize(int size) {
- mManager.setItemSize(size);
- }
-
- /**
- * Set Color of the Item.
- *
- * @param color
- * The color of the Item.
- */
- public void setItemColor(int color) {
- mManager.setItemColor(color);
- }
-
- /**
- * Set Alignment of the Item.
- *
- * @param color
- * The color of the Item.
- */
- public void setAlignment(Layout.Alignment align) {
- mManager.setAlignment(align);
- }
-
- /**
- * Set Background color of View.
- *
- * @param color
- * The background color of view.
- */
- @Override
- public void setBackgroundColor(int color) {
- super.setBackgroundColor(color);
- mBackgroundColor = color;
- }
-
- /**
- * Set html to EditStyledText.
- *
- * @param html
- * The html to be set.
- */
- public void setHtml(String html) {
- mConverter.SetHtml(html);
- }
- /**
- * Check whether editing is started or not.
- *
- * @return Whether editing is started or not.
- */
- public boolean isEditting() {
- return mManager.isEditting();
- }
-
- /**
- * Check whether styled text or not.
- *
- * @return Whether styled text or not.
- */
- public boolean isStyledText() {
- return mManager.isStyledText();
- }
- /**
- * Check whether SoftKey is Blocked or not.
- *
- * @return whether SoftKey is Blocked or not.
- */
- public boolean isSoftKeyBlocked() {
- return mManager.isSoftKeyBlocked();
- }
-
- /**
- * Get the mode of the action.
- *
- * @return The mode of the action.
- */
- public int getEditMode() {
- return mManager.getEditMode();
- }
-
- /**
- * Get the state of the selection.
- *
- * @return The state of the selection.
- */
- public int getSelectState() {
- return mManager.getSelectState();
- }
-
- @Override
- public Bundle getInputExtras(boolean create) {
- if (DBG) {
- Log.d(LOG_TAG, "---getInputExtras");
- }
- Bundle bundle = super.getInputExtras(create);
- if (bundle != null) {
- bundle = new Bundle();
- }
- bundle.putBoolean("allowEmoji", true);
- return bundle;
- }
-
- /**
- * Get the state of the selection.
- *
- * @return The state of the selection.
- */
- public String getHtml() {
- return mConverter.getHtml();
- }
-
- /**
- * Get the state of the selection.
- *
- * @param uris
- * The array of used uris.
- * @return The state of the selection.
- */
- public String getHtml(ArrayList<Uri> uris) {
- mConverter.getUriArray(uris, getText());
- return mConverter.getHtml();
- }
-
- /**
- * Get Background color of View.
- *
- * @return The background color of View.
- */
- public int getBackgroundColor() {
- return mBackgroundColor;
- }
-
- /**
- * Get Foreground color of View.
- *
- * @return The background color of View.
- */
- public int getForeGroundColor(int pos) {
- if (DBG) {
- Log.d(LOG_TAG, "---getForeGroundColor: " + pos);
- }
- if (pos < 0 || pos > getText().length()) {
- Log.e(LOG_TAG, "---getForeGroundColor: Illigal position.");
- return DEFAULT_BACKGROUND_COLOR;
- } else {
- ForegroundColorSpan[] spans =
- getText().getSpans(pos, pos, ForegroundColorSpan.class);
- if (spans.length > 0) {
- return spans[0].getForegroundColor();
- } else {
- return DEFAULT_BACKGROUND_COLOR;
- }
- }
- }
-
- /**
- * Initialize members.
- */
- private void init() {
- if (DBG) {
- Log.d(LOG_TAG, "--- init");
- }
- requestFocus();
- mDefaultBackground = getBackground();
- mBackgroundColor = DEFAULT_BACKGROUND_COLOR;
- mManager = new EditorManager(this);
- mConverter = new StyledTextConverter(this);
- mDialog = new StyledTextDialog(this);
- setMovementMethod(new StyledTextArrowKeyMethod(mManager));
- mManager.blockSoftKey();
- mManager.unblockSoftKey();
- }
-
- /**
- * Show Foreground Color Selecting Dialog.
- */
- private void onShowForegroundColorAlert() {
- mDialog.onShowForegroundColorAlertDialog();
- }
-
- /**
- * Show Background Color Selecting Dialog.
- */
- private void onShowBackgroundColorAlert() {
- mDialog.onShowBackgroundColorAlertDialog();
- }
-
- /**
- * Show Size Selecting Dialog.
- */
- private void onShowSizeAlert() {
- mDialog.onShowSizeAlertDialog();
- }
-
- /**
- * Show Alignment Selecting Dialog.
- */
- private void onShowAlignAlert() {
- mDialog.onShowAlignAlertDialog();
- }
-
- /**
- * Notify hint messages what action is expected to calling class.
- *
- * @param msgId
- * Id of the hint message.
- */
- private void setHintMessage(int msgId) {
- if (mESTInterface != null) {
- mESTInterface.notifyHintMsg(msgId);
- }
- }
-
- /**
- * Notify the event that the mode and state are changed.
- *
- * @param mode
- * Mode of the editing action.
- * @param state
- * Mode of the selection state.
- */
- private void notifyStateChanged(int mode, int state) {
- if (mESTInterface != null) {
- mESTInterface.notifyStateChanged(mode, state);
- }
- }
-
- /**
- * EditorManager manages the flow and status of editing actions.
- */
- private class EditorManager {
- private boolean mEditFlag = false;
- private boolean mSoftKeyBlockFlag = false;
- private int mMode = 0;
- private int mState = 0;
- private int mCurStart = 0;
- private int mCurEnd = 0;
- private EditStyledText mEST;
-
- EditorManager(EditStyledText est) {
- mEST = est;
- }
-
- public void onStartEdit() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onStartEdit");
- }
- Log.d(LOG_TAG, "--- onstartedit:");
- handleResetEdit();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onEndEdit() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onEndEdit");
- }
- handleCancel();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartCopy() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onStartCopy");
- }
- handleCopy();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartCut() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onStartCut");
- }
- handleCut();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartPaste() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onStartPaste");
- }
- handlePaste();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartSize() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onStartSize");
- }
- handleSize();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartAlign() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onStartAlignRight");
- }
- handleAlign();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartColor() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickColor");
- }
- handleColor();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartBackgroundColor() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickColor");
- }
- mEST.onShowBackgroundColorAlert();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onStartSelect() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickSelect");
- }
- mMode = MODE_SELECT;
- if (mState == STATE_SELECT_OFF) {
- handleSelect();
- } else {
- unsetSelect();
- handleSelect();
- }
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onCursorMoved() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickView");
- }
- if (mState == STATE_SELECT_ON || mState == STATE_SELECTED) {
- handleSelect();
- mEST.notifyStateChanged(mMode, mState);
- }
- }
-
- public void onStartSelectAll() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickSelectAll");
- }
- handleSelectAll();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onFixSelectedItem() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickComplete");
- }
- handleComplete();
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onInsertImage(Uri uri) {
- if (DBG) {
- Log.d(LOG_TAG, "--- onInsertImage by URI: " + uri.getPath()
- + "," + uri.toString());
- }
- insertImageSpan(new ImageSpan(mEST.getContext(), uri));
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onInsertImage(int resID) {
- if (DBG) {
- Log.d(LOG_TAG, "--- onInsertImage by resID");
- }
- insertImageSpan(new ImageSpan(mEST.getContext(), resID));
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onInsertHorizontalLine() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onInsertHorizontalLine:");
- }
- insertImageSpan(new HorizontalLineSpan(0xFF000000, mEST));
- mEST.notifyStateChanged(mMode, mState);
- }
-
- public void onClearStyles() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClearStyles");
- }
- Editable txt = mEST.getText();
- int len = txt.length();
- Object[] styles = txt.getSpans(0, len, Object.class);
- for (Object style : styles) {
- if (style instanceof ParagraphStyle ||
- style instanceof QuoteSpan ||
- style instanceof CharacterStyle) {
- if (style instanceof ImageSpan) {
- int start = txt.getSpanStart(style);
- int end = txt.getSpanEnd(style);
- txt.replace(start, end, "");
- }
- txt.removeSpan(style);
- }
- }
- mEST.setBackgroundDrawable(mEST.mDefaultBackground);
- mEST.mBackgroundColor = DEFAULT_BACKGROUND_COLOR;
- }
-
- public void setItemSize(int size) {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickSizeItem");
- }
- if (mState == STATE_SELECTED || mState == STATE_SELECT_FIX) {
- changeSizeSelectedText(size);
- handleResetEdit();
- }
- }
-
- public void setItemColor(int color) {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickColorItem");
- }
- if (mState == STATE_SELECTED || mState == STATE_SELECT_FIX) {
- changeColorSelectedText(color);
- handleResetEdit();
- }
- }
-
- public void setAlignment(Layout.Alignment align) {
- if (DBG) {
- Log.d(LOG_TAG, "--- onClickColorItem");
- }
- if (mState == STATE_SELECTED || mState == STATE_SELECT_FIX) {
- changeAlign(align);
- handleResetEdit();
- }
- }
-
- public boolean isEditting() {
- return mEditFlag;
- }
-
- /* If the style of the span is added, add check case for that style */
- public boolean isStyledText() {
- Editable txt = mEST.getText();
- int len = txt.length();
- if (txt.getSpans(0, len -1, ParagraphStyle.class).length > 0 ||
- txt.getSpans(0, len -1, QuoteSpan.class).length > 0 ||
- txt.getSpans(0, len -1, CharacterStyle.class).length > 0 ||
- mEST.mBackgroundColor != DEFAULT_BACKGROUND_COLOR) {
- return true;
- }
- return false;
- }
-
- public boolean isSoftKeyBlocked() {
- return mSoftKeyBlockFlag;
- }
-
- public int getEditMode() {
- return mMode;
- }
-
- public int getSelectState() {
- return mState;
- }
-
- public int getSelectionStart() {
- return mCurStart;
- }
-
- public int getSelectionEnd() {
- return mCurEnd;
- }
-
- private void doNextHandle() {
- if (DBG) {
- Log.d(LOG_TAG, "--- doNextHandle: " + mMode + "," + mState);
- }
- switch (mMode) {
- case MODE_COPY:
- handleCopy();
- break;
- case MODE_CUT:
- handleCut();
- break;
- case MODE_PASTE:
- handlePaste();
- break;
- case MODE_SIZE:
- handleSize();
- break;
- case MODE_COLOR:
- handleColor();
- break;
- case MODE_ALIGN:
- handleAlign();
- break;
- default:
- break;
- }
- }
-
- private void handleCancel() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleCancel");
- }
- mMode = MODE_NOTHING;
- mState = STATE_SELECT_OFF;
- mEditFlag = false;
- Log.d(LOG_TAG, "--- handleCancel:" + mEST.getInputType());
- unblockSoftKey();
- unsetSelect();
- }
-
- private void handleComplete() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleComplete");
- }
- if (!mEditFlag) {
- return;
- }
- if (mState == STATE_SELECTED) {
- mState = STATE_SELECT_FIX;
- }
- doNextHandle();
- }
-
- private void handleTextViewFunc(int mode, int id) {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleTextView: " + mMode + "," + mState +
- "," + id);
- }
- if (!mEditFlag) {
- return;
- }
- if (mMode == MODE_NOTHING || mMode == MODE_SELECT) {
- mMode = mode;
- if (mState == STATE_SELECTED) {
- mState = STATE_SELECT_FIX;
- handleTextViewFunc(mode, id);
- } else {
- handleSelect();
- }
- } else if (mMode != mode) {
- handleCancel();
- mMode = mode;
- handleTextViewFunc(mode, id);
- } else if (mState == STATE_SELECT_FIX) {
- mEST.onTextContextMenuItem(id);
- handleResetEdit();
- }
- }
-
- private void handleCopy() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleCopy: " + mMode + "," + mState);
- }
- handleTextViewFunc(MODE_COPY, android.R.id.copy);
- }
-
- private void handleCut() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleCopy: " + mMode + "," + mState);
- }
- handleTextViewFunc(MODE_CUT, android.R.id.cut);
- }
-
- private void handlePaste() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handlePaste");
- }
- if (!mEditFlag) {
- return;
- }
- mEST.onTextContextMenuItem(android.R.id.paste);
- }
-
- private void handleSetSpan(int mode) {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleSetSpan:" + mEditFlag + ","
- + mState + ',' + mMode);
- }
- if (!mEditFlag) {
- Log.e(LOG_TAG, "--- handleSetSpan: Editing is not started.");
- return;
- }
- if (mMode == MODE_NOTHING || mMode == MODE_SELECT) {
- mMode = mode;
- if (mState == STATE_SELECTED) {
- mState = STATE_SELECT_FIX;
- handleSetSpan(mode);
- } else {
- handleSelect();
- }
- } else if (mMode != mode) {
- handleCancel();
- mMode = mode;
- handleSetSpan(mode);
- } else {
- if (mState == STATE_SELECT_FIX) {
- mEST.setHintMessage(HINT_MSG_NULL);
- switch (mode) {
- case MODE_COLOR:
- mEST.onShowForegroundColorAlert();
- break;
- case MODE_SIZE:
- mEST.onShowSizeAlert();
- break;
- case MODE_ALIGN:
- mEST.onShowAlignAlert();
- break;
- default:
- Log.e(LOG_TAG, "--- handleSetSpan: invalid mode.");
- break;
- }
- } else {
- Log.d(LOG_TAG, "--- handleSetSpan: do nothing.");
- }
- }
- }
-
- private void handleSize() {
- handleSetSpan(MODE_SIZE);
- }
-
- private void handleColor() {
- handleSetSpan(MODE_COLOR);
- }
-
- private void handleAlign() {
- handleSetSpan(MODE_ALIGN);
- }
-
- private void handleSelect() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleSelect:" + mEditFlag + "," + mState);
- }
- if (!mEditFlag) {
- return;
- }
- if (mState == STATE_SELECT_OFF) {
- if (isTextSelected()) {
- Log.e(LOG_TAG, "Selection is off, but selected");
- }
- setSelectStartPos();
- blockSoftKey();
- mEST.setHintMessage(HINT_MSG_SELECT_END);
- } else if (mState == STATE_SELECT_ON) {
- if (isTextSelected()) {
- Log.e(LOG_TAG, "Selection now start, but selected");
- }
- setSelectedEndPos();
- mEST.setHintMessage(HINT_MSG_PUSH_COMPETE);
- doNextHandle();
- } else if (mState == STATE_SELECTED) {
- if (!isTextSelected()) {
- Log.e(LOG_TAG, "Selection is done, but not selected");
- }
- setSelectedEndPos();
- doNextHandle();
- }
- }
-
- private void handleSelectAll() {
- if (DBG) {
- Log.d(LOG_TAG, "--- handleSelectAll");
- }
- if (!mEditFlag) {
- return;
- }
- mEST.selectAll();
- mState = STATE_SELECTED;
- }
-
- private void handleResetEdit() {
- if (DBG) {
- Log.d(LOG_TAG, "Reset Editor");
- }
- blockSoftKey();
- handleCancel();
- mEditFlag = true;
- mEST.setHintMessage(HINT_MSG_SELECT_START);
- }
-
- private void setSelection() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onSelect:" + mCurStart + "," + mCurEnd);
- }
- if (mCurStart >= 0 && mCurStart <= mEST.getText().length()
- && mCurEnd >= 0 && mCurEnd <= mEST.getText().length()) {
- if (mCurStart < mCurEnd) {
- mEST.setSelection(mCurStart, mCurEnd);
- } else {
- mEST.setSelection(mCurEnd, mCurStart);
- }
- mState = STATE_SELECTED;
- } else {
- Log.e(LOG_TAG,
- "Select is on, but cursor positions are illigal.:"
- + mEST.getText().length() + "," + mCurStart
- + "," + mCurEnd);
- }
- }
-
- private void unsetSelect() {
- if (DBG) {
- Log.d(LOG_TAG, "--- offSelect");
- }
- int currpos = mEST.getSelectionStart();
- mEST.setSelection(currpos, currpos);
- mState = STATE_SELECT_OFF;
- }
-
- private void setSelectStartPos() {
- if (DBG) {
- Log.d(LOG_TAG, "--- setSelectStartPos");
- }
- mCurStart = mEST.getSelectionStart();
- mState = STATE_SELECT_ON;
- }
-
- private void setSelectedEndPos() {
- if (DBG) {
- Log.d(LOG_TAG, "--- setSelectEndPos:");
- }
- if (mEST.getSelectionStart() == mCurStart) {
- setSelectedEndPos(mEST.getSelectionEnd());
- } else {
- setSelectedEndPos(mEST.getSelectionStart());
- }
- }
-
- public void setSelectedEndPos(int pos) {
- if (DBG) {
- Log.d(LOG_TAG, "--- setSelectedEndPos:");
- }
- mCurEnd = pos;
- setSelection();
- }
-
- private boolean isTextSelected() {
- if (DBG) {
- Log.d(LOG_TAG, "--- isTextSelected:" + mCurStart + ","
- + mCurEnd);
- }
- return (mCurStart != mCurEnd)
- && (mState == STATE_SELECTED ||
- mState == STATE_SELECT_FIX);
- }
-
- private void setStyledTextSpan(Object span, int start, int end) {
- if (DBG) {
- Log.d(LOG_TAG, "--- setStyledTextSpan:" + mMode + ","
- + start + "," + end);
- }
- if (start < end) {
- mEST.getText().setSpan(span, start, end,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- } else {
- mEST.getText().setSpan(span, end, start,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- }
-
- private void changeSizeSelectedText(int size) {
- if (DBG) {
- Log.d(LOG_TAG, "--- changeSize:" + size);
- }
- setStyledTextSpan(new AbsoluteSizeSpan(size),
- mCurStart, mCurEnd);
- }
-
- private void changeColorSelectedText(int color) {
- if (DBG) {
- Log.d(LOG_TAG, "--- changeColor:" + color);
- }
- setStyledTextSpan(new ForegroundColorSpan(color),
- mCurStart, mCurEnd);
- }
-
- private void changeAlign(Layout.Alignment align) {
- if (DBG) {
- Log.d(LOG_TAG, "--- changeAlign:" + align);
- }
- setStyledTextSpan(new AlignmentSpan.Standard(align),
- findLineStart(mEST.getText(), mCurStart),
- findLineEnd(mEST.getText(), mCurEnd));
- }
-
- private int findLineStart(Editable text, int current) {
- if (DBG) {
- Log.d(LOG_TAG, "--- findLineStart: curr:" + current +
- ", length:" + text.length());
- }
- int pos = current;
- for (; pos > 0; pos--) {
- if (text.charAt(pos - 1) == '\n') {
- break;
- }
- }
- return pos;
- }
-
- private void insertImageSpan(ImageSpan span) {
- if (DBG) {
- Log.d(LOG_TAG, "--- insertImageSpan");
- }
- if (span != null) {
- Log.d(LOG_TAG, "--- insertimagespan:" + span.getDrawable().getIntrinsicHeight() + "," + span.getDrawable().getIntrinsicWidth());
- Log.d(LOG_TAG, "--- insertimagespan:" + span.getDrawable().getClass());
- int curpos = mEST.getSelectionStart();
- mEST.getText().insert(curpos, "\uFFFC");
- mEST.getText().setSpan(span, curpos, curpos + 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- mEST.notifyStateChanged(mMode, mState);
- } else {
- Log.e(LOG_TAG, "--- insertImageSpan: null span was inserted");
- }
- }
-
- private int findLineEnd(Editable text, int current) {
- if (DBG) {
- Log.d(LOG_TAG, "--- findLineEnd: curr:" + current +
- ", length:" + text.length());
- }
- int pos = current;
- for (; pos < text.length(); pos++) {
- if (pos > 0 && text.charAt(pos - 1) == '\n') {
- break;
- }
- }
- return pos;
- }
-
- private void blockSoftKey() {
- if (DBG) {
- Log.d(LOG_TAG, "--- blockSoftKey:");
- }
- InputMethodManager imm = (InputMethodManager) mEST.getContext().
- getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(mEST.getWindowToken(), 0);
- mEST.setOnClickListener(
- new OnClickListener() {
- public void onClick(View v) {
- Log.d(LOG_TAG, "--- ontrackballclick:");
- onFixSelectedItem();
- }
- });
- mSoftKeyBlockFlag = true;
- }
-
- private void unblockSoftKey() {
- if (DBG) {
- Log.d(LOG_TAG, "--- unblockSoftKey:");
- }
- mEST.setOnClickListener(null);
- mSoftKeyBlockFlag = false;
- }
- }
-
- private class StyledTextConverter {
- private EditStyledText mEST;
-
- public StyledTextConverter(EditStyledText est) {
- mEST = est;
- }
-
- public String getHtml() {
- String htmlBody = Html.toHtml(mEST.getText());
- if (DBG) {
- Log.d(LOG_TAG, "--- getConvertedBody:" + htmlBody);
- }
- return htmlBody;
- }
-
- public void getUriArray(ArrayList<Uri> uris, Editable text) {
- uris.clear();
- if (DBG) {
- Log.d(LOG_TAG, "--- getUriArray:");
- }
- int len = text.length();
- int next;
- for (int i = 0; i < text.length(); i = next) {
- next = text.nextSpanTransition(i, len, ImageSpan.class);
- ImageSpan[] images = text.getSpans(i, next, ImageSpan.class);
- for (int j = 0; j < images.length; j++) {
- if (DBG) {
- Log.d(LOG_TAG, "--- getUriArray: foundArray" +
- ((ImageSpan) images[j]).getSource());
- }
- uris.add(Uri.parse(
- ((ImageSpan) images[j]).getSource()));
- }
- }
- }
-
- public void SetHtml (String html) {
- final Spanned spanned = Html.fromHtml(html, new Html.ImageGetter() {
- public Drawable getDrawable(String src) {
- Log.d(LOG_TAG, "--- sethtml: src="+src);
- if (src.startsWith("content://")) {
- Uri uri = Uri.parse(src);
- try {
- InputStream is = mEST.getContext().getContentResolver().openInputStream(uri);
- Bitmap bitmap = BitmapFactory.decodeStream(is);
- Drawable drawable = new BitmapDrawable(
- getContext().getResources(), bitmap);
- drawable.setBounds(0, 0,
- drawable.getIntrinsicWidth(),
- drawable.getIntrinsicHeight());
- is.close();
- return drawable;
- } catch (Exception e) {
- Log.e(LOG_TAG, "--- set html: Failed to loaded content " + uri, e);
- return null;
- }
- }
- Log.d(LOG_TAG, " unknown src="+src);
- return null;
- }
- }, null);
- mEST.setText(spanned);
- }
- }
-
- private class StyledTextDialog {
- Builder mBuilder;
- CharSequence mColorTitle;
- CharSequence mSizeTitle;
- CharSequence mAlignTitle;
- CharSequence[] mColorNames;
- CharSequence[] mColorInts;
- CharSequence[] mSizeNames;
- CharSequence[] mSizeDisplayInts;
- CharSequence[] mSizeSendInts;
- CharSequence[] mAlignNames;
- EditStyledText mEST;
-
- public StyledTextDialog(EditStyledText est) {
- mEST = est;
- }
-
- public void setBuilder(Builder builder) {
- mBuilder = builder;
- }
-
- public void setColorAlertParams(CharSequence colortitle,
- CharSequence[] colornames, CharSequence[] colorints) {
- mColorTitle = colortitle;
- mColorNames = colornames;
- mColorInts = colorints;
- }
-
- public void setSizeAlertParams(CharSequence sizetitle,
- CharSequence[] sizenames, CharSequence[] sizedisplayints,
- CharSequence[] sizesendints) {
- mSizeTitle = sizetitle;
- mSizeNames = sizenames;
- mSizeDisplayInts = sizedisplayints;
- mSizeSendInts = sizesendints;
- }
-
- public void setAlignAlertParams(CharSequence aligntitle,
- CharSequence[] alignnames) {
- mAlignTitle = aligntitle;
- mAlignNames = alignnames;
- }
-
- private boolean checkColorAlertParams() {
- if (DBG) {
- Log.d(LOG_TAG, "--- checkParams");
- }
- if (mBuilder == null) {
- Log.e(LOG_TAG, "--- builder is null.");
- return false;
- } else if (mColorTitle == null || mColorNames == null
- || mColorInts == null) {
- Log.e(LOG_TAG, "--- color alert params are null.");
- return false;
- } else if (mColorNames.length != mColorInts.length) {
- Log.e(LOG_TAG, "--- the length of color alert params are "
- + "different.");
- return false;
- }
- return true;
- }
-
- private boolean checkSizeAlertParams() {
- if (DBG) {
- Log.d(LOG_TAG, "--- checkParams");
- }
- if (mBuilder == null) {
- Log.e(LOG_TAG, "--- builder is null.");
- return false;
- } else if (mSizeTitle == null || mSizeNames == null
- || mSizeDisplayInts == null || mSizeSendInts == null) {
- Log.e(LOG_TAG, "--- size alert params are null.");
- return false;
- } else if (mSizeNames.length != mSizeDisplayInts.length
- && mSizeSendInts.length != mSizeDisplayInts.length) {
- Log.e(LOG_TAG, "--- the length of size alert params are "
- + "different.");
- return false;
- }
- return true;
- }
-
- private boolean checkAlignAlertParams() {
- if (DBG) {
- Log.d(LOG_TAG, "--- checkAlignAlertParams");
- }
- if (mBuilder == null) {
- Log.e(LOG_TAG, "--- builder is null.");
- return false;
- } else if (mAlignTitle == null) {
- Log.e(LOG_TAG, "--- align alert params are null.");
- return false;
- }
- return true;
- }
-
- private void onShowForegroundColorAlertDialog() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onShowForegroundColorAlertDialog");
- }
- if (!checkColorAlertParams()) {
- return;
- }
- mBuilder.setTitle(mColorTitle);
- mBuilder.setIcon(0);
- mBuilder.
- setItems(mColorNames,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- Log.d("EETVM", "mBuilder.onclick:" + which);
- int color = Integer.parseInt(
- (String) mColorInts[which], 16) - 0x01000000;
- mEST.setItemColor(color);
- }
- });
- mBuilder.show();
- }
-
- private void onShowBackgroundColorAlertDialog() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onShowBackgroundColorAlertDialog");
- }
- if (!checkColorAlertParams()) {
- return;
- }
- mBuilder.setTitle(mColorTitle);
- mBuilder.setIcon(0);
- mBuilder.
- setItems(mColorNames,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- Log.d("EETVM", "mBuilder.onclick:" + which);
- int color = Integer.parseInt(
- (String) mColorInts[which], 16) - 0x01000000;
- mEST.setBackgroundColor(color);
- }
- });
- mBuilder.show();
- }
-
- private void onShowSizeAlertDialog() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onShowSizeAlertDialog");
- }
- if (!checkSizeAlertParams()) {
- return;
- }
- mBuilder.setTitle(mSizeTitle);
- mBuilder.setIcon(0);
- mBuilder.
- setItems(mSizeNames,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- Log.d(LOG_TAG, "mBuilder.onclick:" + which);
- int size = Integer
- .parseInt((String) mSizeDisplayInts[which]);
- mEST.setItemSize(size);
- }
- });
- mBuilder.show();
- }
-
- private void onShowAlignAlertDialog() {
- if (DBG) {
- Log.d(LOG_TAG, "--- onShowAlignAlertDialog");
- }
- if (!checkAlignAlertParams()) {
- return;
- }
- mBuilder.setTitle(mAlignTitle);
- mBuilder.setIcon(0);
- mBuilder.
- setItems(mAlignNames,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- Log.d(LOG_TAG, "mBuilder.onclick:" + which);
- Layout.Alignment align = Layout.Alignment.ALIGN_NORMAL;
- switch (which) {
- case 0:
- align = Layout.Alignment.ALIGN_NORMAL;
- break;
- case 1:
- align = Layout.Alignment.ALIGN_CENTER;
- break;
- case 2:
- align = Layout.Alignment.ALIGN_OPPOSITE;
- break;
- default:
- break;
- }
- mEST.setAlignment(align);
- }
- });
- mBuilder.show();
- }
- }
-
- private class StyledTextArrowKeyMethod extends ArrowKeyMovementMethod {
- EditorManager mManager;
- StyledTextArrowKeyMethod(EditorManager manager) {
- super();
- mManager = manager;
- }
-
- @Override
- public boolean onKeyDown(TextView widget, Spannable buffer,
- int keyCode, KeyEvent event) {
- if (!mManager.isSoftKeyBlocked()) {
- return super.onKeyDown(widget, buffer, keyCode, event);
- }
- if (executeDown(widget, buffer, keyCode)) {
- return true;
- }
- return false;
- }
-
- private int getEndPos(TextView widget) {
- int end;
- if (widget.getSelectionStart() == mManager.getSelectionStart()) {
- end = widget.getSelectionEnd();
- } else {
- end = widget.getSelectionStart();
- }
- return end;
- }
-
- private boolean up(TextView widget, Spannable buffer) {
- if (DBG) {
- Log.d(LOG_TAG, "--- up:");
- }
- Layout layout = widget.getLayout();
- int end = getEndPos(widget);
- int line = layout.getLineForOffset(end);
- if (line > 0) {
- int to;
- if (layout.getParagraphDirection(line) ==
- layout.getParagraphDirection(line - 1)) {
- float h = layout.getPrimaryHorizontal(end);
- to = layout.getOffsetForHorizontal(line - 1, h);
- } else {
- to = layout.getLineStart(line - 1);
- }
- mManager.setSelectedEndPos(to);
- mManager.onCursorMoved();
- return true;
- }
- return false;
- }
-
- private boolean down(TextView widget, Spannable buffer) {
- if (DBG) {
- Log.d(LOG_TAG, "--- down:");
- }
- Layout layout = widget.getLayout();
- int end = getEndPos(widget);
- int line = layout.getLineForOffset(end);
- if (line < layout.getLineCount() - 1) {
- int to;
- if (layout.getParagraphDirection(line) ==
- layout.getParagraphDirection(line + 1)) {
- float h = layout.getPrimaryHorizontal(end);
- to = layout.getOffsetForHorizontal(line + 1, h);
- } else {
- to = layout.getLineStart(line + 1);
- }
- mManager.setSelectedEndPos(to);
- mManager.onCursorMoved();
- return true;
- }
- return false;
- }
-
- private boolean left(TextView widget, Spannable buffer) {
- if (DBG) {
- Log.d(LOG_TAG, "--- left:");
- }
- Layout layout = widget.getLayout();
- int to = layout.getOffsetToLeftOf(getEndPos(widget));
- mManager.setSelectedEndPos(to);
- mManager.onCursorMoved();
- return true;
- }
-
- private boolean right(TextView widget, Spannable buffer) {
- if (DBG) {
- Log.d(LOG_TAG, "--- right:");
- }
- Layout layout = widget.getLayout();
- int to = layout.getOffsetToRightOf(getEndPos(widget));
- mManager.setSelectedEndPos(to);
- mManager.onCursorMoved();
- return true;
- }
-
- private boolean executeDown(TextView widget, Spannable buffer,
- int keyCode) {
- if (DBG) {
- Log.d(LOG_TAG, "--- executeDown: " + keyCode);
- }
- boolean handled = false;
-
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_UP:
- handled |= up(widget, buffer);
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- handled |= down(widget, buffer);
- break;
- case KeyEvent.KEYCODE_DPAD_LEFT:
- handled |= left(widget, buffer);
- break;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- handled |= right(widget, buffer);
- break;
- case KeyEvent.KEYCODE_DPAD_CENTER:
- mManager.onFixSelectedItem();
- handled = true;
- break;
- }
- return handled;
- }
- }
-
- public class HorizontalLineSpan extends ImageSpan {
- public HorizontalLineSpan(int color, View view) {
- super(new HorizontalLineDrawable(color, view));
- }
- }
- public class HorizontalLineDrawable extends ShapeDrawable {
- private View mView;
- public HorizontalLineDrawable(int color, View view) {
- super(new RectShape());
- mView = view;
- renewColor(color);
- renewBounds(view);
- }
- @Override
- public void draw(Canvas canvas) {
- if (DBG) {
- Log.d(LOG_TAG, "--- draw:");
- }
- renewColor();
- renewBounds(mView);
- super.draw(canvas);
- }
-
- private void renewBounds(View view) {
- if (DBG) {
- int width = mView.getBackground().getBounds().width();
- int height = mView.getBackground().getBounds().height();
- Log.d(LOG_TAG, "--- renewBounds:" + width + "," + height);
- Log.d(LOG_TAG, "--- renewBounds:" + mView.getClass());
- }
- int width = mView.getWidth();
- if (width > 20) {
- width -= 20;
- }
- setBounds(0, 0, width, 2);
- }
- private void renewColor(int color) {
- if (DBG) {
- Log.d(LOG_TAG, "--- renewColor:" + color);
- }
- getPaint().setColor(color);
- }
- private void renewColor() {
- if (DBG) {
- Log.d(LOG_TAG, "--- renewColor:");
- }
- if (mView instanceof View) {
- ImageSpan parent = getParentSpan();
- Editable text = ((EditStyledText)mView).getText();
- int start = text.getSpanStart(parent);
- ForegroundColorSpan[] spans = text.getSpans(start, start, ForegroundColorSpan.class);
- if (spans.length > 0) {
- renewColor(spans[spans.length - 1].getForegroundColor());
- }
- }
- }
- private ImageSpan getParentSpan() {
- if (DBG) {
- Log.d(LOG_TAG, "--- getParentSpan:");
- }
- if (mView instanceof EditStyledText) {
- Editable text = ((EditStyledText)mView).getText();
- ImageSpan[] images = text.getSpans(0, text.length(), ImageSpan.class);
- if (images.length > 0) {
- for (ImageSpan image: images) {
- if (image.getDrawable() == this) {
- return image;
- }
- }
- }
- }
- Log.e(LOG_TAG, "---renewBounds: Couldn't find");
- return null;
- }
- }
-}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index dbbd286..0b62a67 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -92,6 +92,8 @@ public class LockPatternUtils {
public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type";
private final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt";
+ private final static String PASSWORD_HISTORY_KEY = "lockscreen.passwordhistory";
+
private final Context mContext;
private final ContentResolver mContentResolver;
private DevicePolicyManager mDevicePolicyManager;
@@ -138,6 +140,33 @@ public class LockPatternUtils {
return getDevicePolicyManager().getPasswordQuality(null);
}
+ public int getRequestedPasswordHistoryLength() {
+ return getDevicePolicyManager().getPasswordHistoryLength(null);
+ }
+
+ public int getRequestedPasswordMinimumLetters() {
+ return getDevicePolicyManager().getPasswordMinimumLetters(null);
+ }
+
+ public int getRequestedPasswordMinimumUpperCase() {
+ return getDevicePolicyManager().getPasswordMinimumUpperCase(null);
+ }
+
+ public int getRequestedPasswordMinimumLowerCase() {
+ return getDevicePolicyManager().getPasswordMinimumLowerCase(null);
+ }
+
+ public int getRequestedPasswordMinimumNumeric() {
+ return getDevicePolicyManager().getPasswordMinimumNumeric(null);
+ }
+
+ public int getRequestedPasswordMinimumSymbols() {
+ return getDevicePolicyManager().getPasswordMinimumSymbols(null);
+ }
+
+ public int getRequestedPasswordMinimumNonLetter() {
+ return getDevicePolicyManager().getPasswordMinimumNonLetter(null);
+ }
/**
* Returns the actual password mode, as set by keyguard after updating the password.
*
@@ -202,8 +231,36 @@ public class LockPatternUtils {
}
/**
- * Checks to see if the given file exists and contains any data. Returns true if it does,
- * false otherwise.
+ * Check to see if a password matches any of the passwords stored in the
+ * password history.
+ *
+ * @param password The password to check.
+ * @return Whether the password matches any in the history.
+ */
+ public boolean checkPasswordHistory(String password) {
+ String passwordHashString = new String(passwordToHash(password));
+ String passwordHistory = getString(PASSWORD_HISTORY_KEY);
+ if (passwordHistory == null) {
+ return false;
+ }
+ // Password History may be too long...
+ int passwordHashLength = passwordHashString.length();
+ int passwordHistoryLength = getRequestedPasswordHistoryLength();
+ if(passwordHistoryLength == 0) {
+ return false;
+ }
+ int neededPasswordHistoryLength = passwordHashLength * passwordHistoryLength
+ + passwordHistoryLength - 1;
+ if (passwordHistory.length() > neededPasswordHistoryLength) {
+ passwordHistory = passwordHistory.substring(0, neededPasswordHistoryLength);
+ }
+ return passwordHistory.contains(passwordHashString);
+ }
+
+ /**
+ * Checks to see if the given file exists and contains any data. Returns
+ * true if it does, false otherwise.
+ *
* @param filename
* @return true if file exists and is non-empty.
*/
@@ -274,6 +331,11 @@ public class LockPatternUtils {
activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
}
break;
+ case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX:
+ if (isLockPasswordEnabled()) {
+ activePasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+ }
+ break;
}
return activePasswordQuality;
}
@@ -282,8 +344,6 @@ public class LockPatternUtils {
* Clear any lock pattern or password.
*/
public void clearLock() {
- getDevicePolicyManager().setActivePasswordState(
- DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0);
saveLockPassword(null, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
setLockPatternEnabled(false);
saveLockPattern(null);
@@ -296,7 +356,7 @@ public class LockPatternUtils {
*/
public void saveLockPattern(List<LockPatternView.Cell> pattern) {
// Compute the hash
- final byte[] hash = LockPatternUtils.patternToHash(pattern);
+ final byte[] hash = LockPatternUtils.patternToHash(pattern);
try {
// Write the hash to file
RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "rw");
@@ -311,14 +371,15 @@ public class LockPatternUtils {
if (pattern != null) {
setBoolean(PATTERN_EVER_CHOSEN_KEY, true);
setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
- dpm.setActivePasswordState(
- DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, pattern.size());
+ dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, pattern
+ .size(), 0, 0, 0, 0, 0, 0);
} else {
- dpm.setActivePasswordState(
- DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0);
+ dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0,
+ 0, 0, 0, 0, 0);
}
} catch (FileNotFoundException fnfe) {
- // Cant do much, unless we want to fail over to using the settings provider
+ // Cant do much, unless we want to fail over to using the settings
+ // provider
Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename);
} catch (IOException ioe) {
// Cant do much
@@ -376,17 +437,59 @@ public class LockPatternUtils {
DevicePolicyManager dpm = getDevicePolicyManager();
if (password != null) {
int computedQuality = computePasswordQuality(password);
- setLong(PASSWORD_TYPE_KEY, computedQuality);
+ setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality));
if (computedQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
- dpm.setActivePasswordState(computedQuality, password.length());
+ int letters = 0;
+ int uppercase = 0;
+ int lowercase = 0;
+ int numbers = 0;
+ int symbols = 0;
+ int nonletter = 0;
+ for (int i = 0; i < password.length(); i++) {
+ char c = password.charAt(i);
+ if (c >= 'A' && c <= 'Z') {
+ letters++;
+ uppercase++;
+ } else if (c >= 'a' && c <= 'z') {
+ letters++;
+ lowercase++;
+ } else if (c >= '0' && c <= '9') {
+ numbers++;
+ nonletter++;
+ } else {
+ symbols++;
+ nonletter++;
+ }
+ }
+ dpm.setActivePasswordState(Math.max(quality, computedQuality), password
+ .length(), letters, uppercase, lowercase, numbers, symbols, nonletter);
} else {
// The password is not anything.
dpm.setActivePasswordState(
- DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0);
+ DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0);
+ }
+ // Add the password to the password history. We assume all
+ // password
+ // hashes have the same length for simplicity of implementation.
+ String passwordHistory = getString(PASSWORD_HISTORY_KEY);
+ if (passwordHistory == null) {
+ passwordHistory = new String();
+ }
+ int passwordHistoryLength = getRequestedPasswordHistoryLength();
+ if (passwordHistoryLength == 0) {
+ passwordHistory = "";
+ } else {
+ passwordHistory = new String(hash) + "," + passwordHistory;
+ // Cut it to contain passwordHistoryLength hashes
+ // and passwordHistoryLength -1 commas.
+ passwordHistory = passwordHistory.substring(0, Math.min(hash.length
+ * passwordHistoryLength + passwordHistoryLength - 1, passwordHistory
+ .length()));
}
+ setString(PASSWORD_HISTORY_KEY, passwordHistory);
} else {
dpm.setActivePasswordState(
- DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0);
+ DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0);
}
} catch (FileNotFoundException fnfe) {
// Cant do much, unless we want to fail over to using the settings provider
@@ -526,7 +629,8 @@ public class LockPatternUtils {
return savedPasswordExists() &&
(mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
|| mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
- || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC);
+ || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
+ || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
}
/**
@@ -650,12 +754,21 @@ public class LockPatternUtils {
android.provider.Settings.Secure.putLong(mContentResolver, secureSettingKey, value);
}
+ private String getString(String secureSettingKey) {
+ return android.provider.Settings.Secure.getString(mContentResolver, secureSettingKey);
+ }
+
+ private void setString(String secureSettingKey, String value) {
+ android.provider.Settings.Secure.putString(mContentResolver, secureSettingKey, value);
+ }
+
public boolean isSecure() {
long mode = getKeyguardStoredPasswordQuality();
final boolean isPattern = mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
final boolean isPassword = mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
|| mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
- || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+ || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
+ || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
final boolean secure = isPattern && isLockPatternEnabled() && savedPatternExists()
|| isPassword && savedPasswordExists();
return secure;
diff --git a/core/java/com/android/internal/widget/SlidingTab.java b/core/java/com/android/internal/widget/SlidingTab.java
index 9152729..3218ba8 100644
--- a/core/java/com/android/internal/widget/SlidingTab.java
+++ b/core/java/com/android/internal/widget/SlidingTab.java
@@ -17,10 +17,8 @@
package com.android.internal.widget;
import android.content.Context;
-import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Vibrator;
@@ -38,6 +36,7 @@ import android.view.animation.Animation.AnimationListener;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ImageView.ScaleType;
+
import com.android.internal.R;
/**
@@ -69,21 +68,21 @@ public class SlidingTab extends ViewGroup {
private int mGrabbedState = OnTriggerListener.NO_HANDLE;
private boolean mTriggered = false;
private Vibrator mVibrator;
- private float mDensity; // used to scale dimensions for bitmaps.
+ private final float mDensity; // used to scale dimensions for bitmaps.
/**
* Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
- private int mOrientation;
+ private final int mOrientation;
- private Slider mLeftSlider;
- private Slider mRightSlider;
+ private final Slider mLeftSlider;
+ private final Slider mRightSlider;
private Slider mCurrentSlider;
private boolean mTracking;
private float mThreshold;
private Slider mOtherSlider;
private boolean mAnimating;
- private Rect mTmpRect;
+ private final Rect mTmpRect;
/**
* Listener used to reset the view when the current animation completes.
@@ -608,14 +607,7 @@ public class SlidingTab extends ViewGroup {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
- mTracking = false;
- mTriggered = false;
- mOtherSlider.show(true);
- mCurrentSlider.reset(false);
- mCurrentSlider.hideTarget();
- mCurrentSlider = null;
- mOtherSlider = null;
- setGrabbedState(OnTriggerListener.NO_HANDLE);
+ cancelGrab();
break;
}
}
@@ -623,6 +615,17 @@ public class SlidingTab extends ViewGroup {
return mTracking || super.onTouchEvent(event);
}
+ private void cancelGrab() {
+ mTracking = false;
+ mTriggered = false;
+ mOtherSlider.show(true);
+ mCurrentSlider.reset(false);
+ mCurrentSlider.hideTarget();
+ mCurrentSlider = null;
+ mOtherSlider = null;
+ setGrabbedState(OnTriggerListener.NO_HANDLE);
+ }
+
void startAnimating(final boolean holdAfter) {
mAnimating = true;
final Animation trans1;
@@ -832,6 +835,17 @@ public class SlidingTab extends ViewGroup {
}
}
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ // When visibility changes and the user has a tab selected, unselect it and
+ // make sure their callback gets called.
+ if (changedView == this && visibility != VISIBLE
+ && mGrabbedState != OnTriggerListener.NO_HANDLE) {
+ cancelGrab();
+ }
+ }
+
/**
* Sets the current grabbed state, and dispatches a grabbed state change
* event to our listener.