summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/accessibilityservice/AccessibilityServiceInfo.java52
-rw-r--r--core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl18
-rw-r--r--core/java/android/accounts/AccountManagerService.java3
-rw-r--r--core/java/android/accounts/ChooseTypeAndAccountActivity.java16
-rw-r--r--core/java/android/animation/LayoutTransition.java88
-rwxr-xr-xcore/java/android/animation/ValueAnimator.java359
-rw-r--r--core/java/android/app/ActivityManager.java26
-rw-r--r--core/java/android/app/ActivityThread.java62
-rw-r--r--core/java/android/app/AlarmManager.java53
-rw-r--r--core/java/android/app/ApplicationThreadNative.java55
-rw-r--r--core/java/android/app/IApplicationThread.java5
-rw-r--r--core/java/android/app/Instrumentation.java22
-rw-r--r--core/java/android/app/SearchDialog.java1
-rw-r--r--core/java/android/app/StatusBarManager.java5
-rw-r--r--core/java/android/app/UiModeManager.java2
-rw-r--r--core/java/android/app/WallpaperManager.java30
-rw-r--r--core/java/android/bluetooth/BluetoothAdapter.java21
-rw-r--r--core/java/android/content/AsyncTaskLoader.java20
-rw-r--r--core/java/android/content/CancelationSignal.java164
-rw-r--r--core/java/android/content/ContentProvider.java110
-rw-r--r--core/java/android/content/ContentProviderClient.java15
-rw-r--r--core/java/android/content/ContentProviderNative.java41
-rw-r--r--core/java/android/content/ContentResolver.java66
-rw-r--r--core/java/android/content/ContentService.java25
-rw-r--r--core/java/android/content/CursorLoader.java39
-rw-r--r--core/java/android/content/ICancelationSignal.aidl (renamed from core/java/android/nfc/LlcpPacket.aidl)8
-rw-r--r--core/java/android/content/IContentProvider.java5
-rw-r--r--core/java/android/content/Intent.java174
-rw-r--r--core/java/android/content/OperationCanceledException.java32
-rw-r--r--core/java/android/content/SyncManager.java116
-rw-r--r--core/java/android/content/pm/ResolveInfo.java4
-rw-r--r--core/java/android/content/res/Configuration.java5
-rwxr-xr-xcore/java/android/content/res/Resources.java3
-rw-r--r--core/java/android/database/AbstractCursor.java19
-rw-r--r--core/java/android/database/BulkCursorNative.java4
-rw-r--r--core/java/android/database/ContentObservable.java55
-rw-r--r--core/java/android/database/ContentObserver.java191
-rw-r--r--core/java/android/database/CrossProcessCursorWrapper.java8
-rw-r--r--core/java/android/database/Cursor.java1
-rw-r--r--core/java/android/database/CursorToBulkCursorAdaptor.java16
-rw-r--r--core/java/android/database/CursorWindow.java6
-rw-r--r--core/java/android/database/DataSetObservable.java15
-rw-r--r--core/java/android/database/DatabaseUtils.java26
-rw-r--r--core/java/android/database/IBulkCursor.java12
-rwxr-xr-xcore/java/android/database/IContentObserver.aidl4
-rw-r--r--core/java/android/database/Observable.java13
-rw-r--r--core/java/android/database/sqlite/DatabaseConnectionPool.java348
-rw-r--r--core/java/android/database/sqlite/SQLiteClosable.java22
-rw-r--r--core/java/android/database/sqlite/SQLiteCompiledSql.java158
-rw-r--r--core/java/android/database/sqlite/SQLiteConnection.java1315
-rw-r--r--core/java/android/database/sqlite/SQLiteConnectionPool.java970
-rw-r--r--core/java/android/database/sqlite/SQLiteCursor.java147
-rw-r--r--core/java/android/database/sqlite/SQLiteCursorDriver.java2
-rw-r--r--core/java/android/database/sqlite/SQLiteCustomFunction.java53
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java1891
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java167
-rw-r--r--core/java/android/database/sqlite/SQLiteDebug.java111
-rw-r--r--core/java/android/database/sqlite/SQLiteDirectCursorDriver.java43
-rw-r--r--core/java/android/database/sqlite/SQLiteGlobal.java67
-rw-r--r--core/java/android/database/sqlite/SQLiteOpenHelper.java21
-rw-r--r--core/java/android/database/sqlite/SQLiteProgram.java379
-rw-r--r--core/java/android/database/sqlite/SQLiteQuery.java163
-rw-r--r--core/java/android/database/sqlite/SQLiteQueryBuilder.java70
-rw-r--r--core/java/android/database/sqlite/SQLiteSession.java963
-rw-r--r--core/java/android/database/sqlite/SQLiteStatement.java248
-rw-r--r--core/java/android/database/sqlite/SQLiteStatementInfo.java (renamed from core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java)26
-rw-r--r--core/java/android/hardware/Camera.java61
-rw-r--r--core/java/android/inputmethodservice/ExtractEditText.java3
-rw-r--r--core/java/android/net/ConnectivityManager.java5
-rw-r--r--core/java/android/net/DhcpStateMachine.java34
-rw-r--r--core/java/android/net/InterfaceConfiguration.java139
-rw-r--r--core/java/android/net/NetworkIdentity.java2
-rw-r--r--core/java/android/net/NetworkInfo.java17
-rw-r--r--core/java/android/net/NetworkPolicy.java17
-rw-r--r--core/java/android/net/NetworkStats.java121
-rw-r--r--core/java/android/net/NetworkStatsHistory.java78
-rw-r--r--core/java/android/net/NetworkTemplate.java10
-rw-r--r--core/java/android/net/TrafficStats.java16
-rw-r--r--core/java/android/net/Uri.java149
-rw-r--r--core/java/android/net/http/CertificateChainValidator.java28
-rw-r--r--core/java/android/net/http/HttpResponseCache.java12
-rw-r--r--core/java/android/nfc/FormatException.java4
-rw-r--r--core/java/android/nfc/INfcAdapter.aidl3
-rw-r--r--core/java/android/nfc/LlcpPacket.java85
-rw-r--r--core/java/android/nfc/NdefMessage.java229
-rw-r--r--core/java/android/nfc/NdefRecord.java905
-rw-r--r--core/java/android/nfc/NfcAdapter.java135
-rw-r--r--core/java/android/nfc/NfcManager.java4
-rw-r--r--core/java/android/nfc/Tag.java19
-rw-r--r--core/java/android/nfc/tech/BasicTagTechnology.java12
-rw-r--r--core/java/android/nfc/tech/Ndef.java12
-rw-r--r--core/java/android/os/AsyncTask.java12
-rw-r--r--core/java/android/os/Binder.java3
-rw-r--r--core/java/android/os/Build.java65
-rw-r--r--core/java/android/os/IPowerManager.aidl1
-rw-r--r--core/java/android/os/Process.java48
-rw-r--r--core/java/android/os/RecoverySystem.java10
-rw-r--r--core/java/android/provider/ContactsContract.java33
-rw-r--r--core/java/android/provider/MediaStore.java8
-rw-r--r--core/java/android/provider/Settings.java104
-rw-r--r--core/java/android/provider/UserDictionary.java75
-rw-r--r--core/java/android/server/BluetoothAdapterStateMachine.java11
-rw-r--r--core/java/android/server/BluetoothPanProfileHandler.java10
-rw-r--r--core/java/android/service/textservice/SpellCheckerService.java29
-rw-r--r--core/java/android/service/wallpaper/WallpaperService.java43
-rw-r--r--core/java/android/speech/tts/AudioMessageParams.java38
-rw-r--r--core/java/android/speech/tts/AudioPlaybackHandler.java591
-rw-r--r--core/java/android/speech/tts/AudioPlaybackQueueItem.java (renamed from core/java/android/speech/tts/BlockingMediaPlayer.java)101
-rw-r--r--core/java/android/speech/tts/BlockingAudioTrack.java338
-rw-r--r--core/java/android/speech/tts/EventLogTags.logtags4
-rw-r--r--core/java/android/speech/tts/EventLogger.java23
-rw-r--r--core/java/android/speech/tts/ITextToSpeechService.aidl32
-rw-r--r--core/java/android/speech/tts/MessageParams.java47
-rw-r--r--core/java/android/speech/tts/PlaybackQueueItem.java27
-rw-r--r--core/java/android/speech/tts/PlaybackSynthesisCallback.java62
-rw-r--r--core/java/android/speech/tts/SilencePlaybackQueueItem.java (renamed from core/java/android/speech/tts/SilenceMessageParams.java)27
-rw-r--r--core/java/android/speech/tts/SynthesisMessageParams.java159
-rw-r--r--core/java/android/speech/tts/SynthesisPlaybackQueueItem.java245
-rwxr-xr-xcore/java/android/speech/tts/TextToSpeech.java59
-rw-r--r--core/java/android/speech/tts/TextToSpeechService.java191
-rw-r--r--core/java/android/text/MeasuredText.java3
-rw-r--r--core/java/android/text/format/DateUtils.java133
-rw-r--r--core/java/android/text/format/Time.java3
-rw-r--r--core/java/android/text/method/ArrowKeyMovementMethod.java2
-rw-r--r--core/java/android/text/style/SuggestionSpan.java10
-rw-r--r--core/java/android/util/DisplayMetrics.java7
-rw-r--r--core/java/android/util/LocalLog.java56
-rw-r--r--core/java/android/util/LocaleUtil.java2
-rw-r--r--core/java/android/util/LruCache.java21
-rw-r--r--core/java/android/util/SparseLongArray.java247
-rw-r--r--core/java/android/view/Choreographer.java382
-rw-r--r--core/java/android/view/DisplayEventReceiver.java115
-rw-r--r--core/java/android/view/DisplayList.java10
-rw-r--r--core/java/android/view/GLES20Canvas.java127
-rw-r--r--core/java/android/view/GLES20DisplayList.java10
-rw-r--r--core/java/android/view/GLES20Layer.java7
-rw-r--r--core/java/android/view/GestureDetector.java11
-rw-r--r--core/java/android/view/HardwareCanvas.java6
-rw-r--r--core/java/android/view/HardwareLayer.java5
-rw-r--r--core/java/android/view/HardwareRenderer.java87
-rwxr-xr-xcore/java/android/view/InputEvent.java72
-rw-r--r--core/java/android/view/InputEventConsistencyVerifier.java11
-rw-r--r--core/java/android/view/InputEventReceiver.java141
-rw-r--r--core/java/android/view/InputHandler.java43
-rw-r--r--core/java/android/view/InputQueue.java131
-rwxr-xr-xcore/java/android/view/KeyEvent.java15
-rw-r--r--core/java/android/view/MotionEvent.java21
-rw-r--r--core/java/android/view/VelocityTracker.java3
-rw-r--r--core/java/android/view/View.java326
-rw-r--r--core/java/android/view/ViewConfiguration.java76
-rw-r--r--core/java/android/view/ViewDebug.java13
-rw-r--r--core/java/android/view/ViewGroup.java56
-rw-r--r--core/java/android/view/ViewPropertyAnimator.java6
-rw-r--r--core/java/android/view/ViewRootImpl.java1302
-rw-r--r--core/java/android/view/ViewTreeObserver.java27
-rw-r--r--core/java/android/view/VolumePanel.java2
-rw-r--r--core/java/android/view/WindowManagerPolicy.java3
-rwxr-xr-xcore/java/android/view/WindowOrientationListener.java387
-rw-r--r--core/java/android/view/accessibility/AccessibilityEvent.java6
-rw-r--r--core/java/android/view/accessibility/AccessibilityInteractionClient.java60
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeInfo.java259
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeProvider.java130
-rw-r--r--core/java/android/view/accessibility/AccessibilityRecord.java39
-rw-r--r--core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl6
-rw-r--r--core/java/android/view/animation/Animation.java2
-rw-r--r--core/java/android/view/animation/AnimationSet.java17
-rw-r--r--core/java/android/view/animation/AnimationUtils.java8
-rw-r--r--core/java/android/view/animation/RotateAnimation.java17
-rw-r--r--core/java/android/view/animation/ScaleAnimation.java17
-rw-r--r--core/java/android/view/inputmethod/BaseInputConnection.java16
-rw-r--r--core/java/android/view/inputmethod/EditorInfo.java16
-rw-r--r--core/java/android/view/inputmethod/InputConnection.java16
-rw-r--r--core/java/android/view/inputmethod/InputConnectionWrapper.java4
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java44
-rw-r--r--core/java/android/view/inputmethod/InputMethodSubtype.java107
-rw-r--r--core/java/android/view/textservice/SpellCheckerSession.java63
-rw-r--r--core/java/android/view/textservice/SpellCheckerSubtype.java46
-rw-r--r--core/java/android/view/textservice/SuggestionsInfo.java76
-rw-r--r--core/java/android/view/textservice/TextServicesManager.java3
-rw-r--r--core/java/android/webkit/BrowserFrame.java1
-rwxr-xr-xcore/java/android/webkit/GeolocationPermissions.java101
-rw-r--r--core/java/android/webkit/HTML5VideoFullScreen.java17
-rw-r--r--core/java/android/webkit/HTML5VideoInline.java5
-rw-r--r--core/java/android/webkit/HTML5VideoView.java55
-rw-r--r--core/java/android/webkit/HTML5VideoViewProxy.java48
-rw-r--r--core/java/android/webkit/HttpAuthHandlerImpl.java1
-rw-r--r--core/java/android/webkit/JniUtil.java10
-rw-r--r--core/java/android/webkit/Network.java4
-rw-r--r--core/java/android/webkit/SearchBox.java2
-rw-r--r--core/java/android/webkit/SelectActionModeCallback.java53
-rw-r--r--core/java/android/webkit/ValueCallback.java5
-rw-r--r--core/java/android/webkit/WebChromeClient.java16
-rw-r--r--core/java/android/webkit/WebCoreThreadWatchdog.java241
-rw-r--r--core/java/android/webkit/WebSettings.java47
-rw-r--r--core/java/android/webkit/WebStorage.java2
-rw-r--r--core/java/android/webkit/WebView.java610
-rw-r--r--core/java/android/webkit/WebViewCore.java544
-rw-r--r--core/java/android/webkit/ZoomManager.java18
-rw-r--r--core/java/android/widget/AbsListView.java23
-rw-r--r--core/java/android/widget/AbsSeekBar.java13
-rw-r--r--core/java/android/widget/AbsSpinner.java15
-rw-r--r--core/java/android/widget/ActivityChooserView.java8
-rw-r--r--core/java/android/widget/AdapterView.java2
-rw-r--r--core/java/android/widget/AdapterViewAnimator.java19
-rw-r--r--core/java/android/widget/AdapterViewFlipper.java16
-rw-r--r--core/java/android/widget/AutoCompleteTextView.java5
-rw-r--r--core/java/android/widget/Button.java17
-rw-r--r--core/java/android/widget/CalendarView.java14
-rw-r--r--core/java/android/widget/CheckBox.java13
-rw-r--r--core/java/android/widget/CheckedTextView.java2
-rw-r--r--core/java/android/widget/Chronometer.java14
-rw-r--r--core/java/android/widget/CompoundButton.java2
-rw-r--r--core/java/android/widget/DatePicker.java13
-rw-r--r--core/java/android/widget/DigitalClock.java14
-rw-r--r--core/java/android/widget/EditText.java14
-rw-r--r--core/java/android/widget/ExpandableListView.java13
-rw-r--r--core/java/android/widget/FrameLayout.java15
-rw-r--r--core/java/android/widget/Gallery.java14
-rw-r--r--core/java/android/widget/GridLayout.java23
-rw-r--r--core/java/android/widget/GridView.java17
-rw-r--r--core/java/android/widget/HorizontalScrollView.java2
-rw-r--r--core/java/android/widget/ImageButton.java14
-rw-r--r--core/java/android/widget/ImageSwitcher.java17
-rw-r--r--core/java/android/widget/ImageView.java13
-rw-r--r--core/java/android/widget/LinearLayout.java16
-rw-r--r--core/java/android/widget/ListView.java17
-rw-r--r--core/java/android/widget/MediaController.java14
-rw-r--r--core/java/android/widget/MultiAutoCompleteTextView.java15
-rw-r--r--core/java/android/widget/NumberPicker.java56
-rw-r--r--core/java/android/widget/ProgressBar.java8
-rw-r--r--core/java/android/widget/QuickContactBadge.java14
-rw-r--r--core/java/android/widget/RadioButton.java13
-rw-r--r--core/java/android/widget/RadioGroup.java14
-rw-r--r--core/java/android/widget/RatingBar.java13
-rw-r--r--core/java/android/widget/RelativeLayout.java63
-rw-r--r--core/java/android/widget/RemoteViewsAdapter.java39
-rw-r--r--core/java/android/widget/ScrollView.java2
-rw-r--r--core/java/android/widget/SearchView.java15
-rw-r--r--core/java/android/widget/SeekBar.java15
-rw-r--r--core/java/android/widget/ShareActionProvider.java1
-rw-r--r--core/java/android/widget/SlidingDrawer.java13
-rw-r--r--core/java/android/widget/SpellChecker.java92
-rw-r--r--core/java/android/widget/Spinner.java16
-rw-r--r--core/java/android/widget/StackView.java14
-rw-r--r--core/java/android/widget/Switch.java13
-rw-r--r--core/java/android/widget/TabHost.java14
-rw-r--r--core/java/android/widget/TabWidget.java29
-rw-r--r--core/java/android/widget/TableLayout.java14
-rw-r--r--core/java/android/widget/TableRow.java14
-rw-r--r--core/java/android/widget/TextSwitcher.java14
-rw-r--r--core/java/android/widget/TextView.java340
-rw-r--r--core/java/android/widget/TimePicker.java13
-rw-r--r--core/java/android/widget/ToggleButton.java13
-rw-r--r--core/java/android/widget/TwoLineListItem.java18
-rw-r--r--core/java/android/widget/VideoView.java15
-rw-r--r--core/java/android/widget/ViewAnimator.java18
-rw-r--r--core/java/android/widget/ViewFlipper.java14
-rw-r--r--core/java/android/widget/ViewSwitcher.java14
-rw-r--r--core/java/android/widget/ZoomButton.java14
-rw-r--r--core/java/android/widget/ZoomControls.java14
-rw-r--r--core/java/com/android/internal/backup/BackupConstants.java1
-rw-r--r--core/java/com/android/internal/net/DNParser.java450
-rw-r--r--core/java/com/android/internal/net/DomainNameValidator.java260
-rw-r--r--core/java/com/android/internal/os/AtomicFile.java11
-rw-r--r--core/java/com/android/internal/os/BatteryStatsImpl.java36
-rw-r--r--core/java/com/android/internal/os/ZygoteInit.java2
-rw-r--r--core/java/com/android/internal/textservice/ISpellCheckerSession.aidl1
-rw-r--r--core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl1
-rw-r--r--core/java/com/android/internal/util/ArrayUtils.java61
-rw-r--r--core/java/com/android/internal/util/FileRotator.java421
-rw-r--r--core/java/com/android/internal/util/IndentingPrintWriter.java63
-rw-r--r--core/java/com/android/internal/view/BaseInputHandler.java36
-rw-r--r--core/java/com/android/internal/view/InputConnectionWrapper.java4
-rw-r--r--core/java/com/android/internal/widget/ActionBarView.java2
-rw-r--r--core/java/com/android/internal/widget/DigitalClock.java2
-rw-r--r--core/java/com/android/internal/widget/EditableInputConnection.java54
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java2
-rw-r--r--core/java/com/android/server/NetworkManagementSocketTagger.java39
-rwxr-xr-xcore/java/com/google/android/mms/pdu/PduParser.java3
279 files changed, 14994 insertions, 8094 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index e5a5e98..eae0a4c 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -28,6 +28,7 @@ import android.content.res.XmlResourceParser;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
+import android.util.TypedValue;
import android.util.Xml;
import android.view.accessibility.AccessibilityEvent;
@@ -182,9 +183,14 @@ public class AccessibilityServiceInfo implements Parcelable {
private boolean mCanRetrieveWindowContent;
/**
- * Description of the accessibility service.
+ * Resource id of the description of the accessibility service.
*/
- private String mDescription;
+ private int mDescriptionResId;
+
+ /**
+ * Non localized description of the accessibility service.
+ */
+ private String mNonLocalizedDescription;
/**
* Creates a new instance.
@@ -256,8 +262,15 @@ public class AccessibilityServiceInfo implements Parcelable {
mCanRetrieveWindowContent = asAttributes.getBoolean(
com.android.internal.R.styleable.AccessibilityService_canRetrieveWindowContent,
false);
- mDescription = asAttributes.getString(
+ TypedValue peekedValue = asAttributes.peekValue(
com.android.internal.R.styleable.AccessibilityService_description);
+ if (peekedValue != null) {
+ mDescriptionResId = peekedValue.resourceId;
+ CharSequence nonLocalizedDescription = peekedValue.coerceToString();
+ if (nonLocalizedDescription != null) {
+ mNonLocalizedDescription = nonLocalizedDescription.toString().trim();
+ }
+ }
asAttributes.recycle();
} catch (NameNotFoundException e) {
throw new XmlPullParserException( "Unable to create context for: "
@@ -331,15 +344,38 @@ public class AccessibilityServiceInfo implements Parcelable {
}
/**
- * Description of the accessibility service.
+ * Gets the non-localized description of the accessibility service.
* <p>
* <strong>Statically set from
* {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
* </p>
* @return The description.
+ *
+ * @deprecated Use {@link #loadDescription(PackageManager)}.
*/
public String getDescription() {
- return mDescription;
+ return mNonLocalizedDescription;
+ }
+
+ /**
+ * The localized description of the accessibility service.
+ * <p>
+ * <strong>Statically set from
+ * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong>
+ * </p>
+ * @return The localized description.
+ */
+ public String loadDescription(PackageManager packageManager) {
+ if (mDescriptionResId == 0) {
+ return mNonLocalizedDescription;
+ }
+ ServiceInfo serviceInfo = mResolveInfo.serviceInfo;
+ CharSequence description = packageManager.getText(serviceInfo.packageName,
+ mDescriptionResId, serviceInfo.applicationInfo);
+ if (description != null) {
+ return description.toString().trim();
+ }
+ return null;
}
/**
@@ -359,7 +395,8 @@ public class AccessibilityServiceInfo implements Parcelable {
parcel.writeParcelable(mResolveInfo, 0);
parcel.writeString(mSettingsActivityName);
parcel.writeInt(mCanRetrieveWindowContent ? 1 : 0);
- parcel.writeString(mDescription);
+ parcel.writeInt(mDescriptionResId);
+ parcel.writeString(mNonLocalizedDescription);
}
private void initFromParcel(Parcel parcel) {
@@ -372,7 +409,8 @@ public class AccessibilityServiceInfo implements Parcelable {
mResolveInfo = parcel.readParcelable(null);
mSettingsActivityName = parcel.readString();
mCanRetrieveWindowContent = (parcel.readInt() == 1);
- mDescription = parcel.readString();
+ mDescriptionResId = parcel.readInt();
+ mNonLocalizedDescription = parcel.readString();
}
@Override
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 7c41082..e53b313 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -33,14 +33,14 @@ interface IAccessibilityServiceConnection {
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
* @param accessibilityWindowId A unique window id.
- * @param accessibilityViewId A unique View accessibility id.
+ * @param accessibilityNodeId A unique view id or virtual descendant id.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
* @param threadId The id of the calling thread.
* @return The current window scale, where zero means a failure.
*/
float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId,
- int accessibilityViewId, int interactionId,
+ long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long threadId);
/**
@@ -51,15 +51,15 @@ interface IAccessibilityServiceConnection {
*
* @param text The searched text.
* @param accessibilityWindowId A unique window id.
- * @param accessibilityViewId A unique View accessibility id from where to start the search.
- * Use {@link android.view.View#NO_ID} to start from the root.
+ * @param accessibilityNodeId A unique view id or virtual descendant id from
+ * where to start the search. Use {@link android.view.View#NO_ID} to start from the root.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
* @param threadId The id of the calling thread.
* @return The current window scale, where zero means a failure.
*/
- float findAccessibilityNodeInfosByViewText(String text, int accessibilityWindowId,
- int accessibilityViewId, int interractionId,
+ float findAccessibilityNodeInfosByText(String text, int accessibilityWindowId,
+ long accessibilityNodeId, int interractionId,
IAccessibilityInteractionConnectionCallback callback, long threadId);
/**
@@ -75,7 +75,7 @@ interface IAccessibilityServiceConnection {
* @param threadId The id of the calling thread.
* @return The current window scale, where zero means a failure.
*/
- float findAccessibilityNodeInfosByViewTextInActiveWindow(String text,
+ float findAccessibilityNodeInfosByTextInActiveWindow(String text,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
long threadId);
@@ -96,14 +96,14 @@ interface IAccessibilityServiceConnection {
* Performs an accessibility action on an {@link AccessibilityNodeInfo}.
*
* @param accessibilityWindowId The id of the window.
- * @param accessibilityViewId A unique View accessibility id.
+ * @param accessibilityNodeId A unique view id or virtual descendant id.
* @param action The action to perform.
* @param interactionId The id of the interaction for matching with the callback result.
* @param callback Callback which to receive the result.
* @param threadId The id of the calling thread.
* @return Whether the action was performed.
*/
- boolean performAccessibilityAction(int accessibilityWindowId, int accessibilityViewId,
+ boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId,
int action, int interactionId, IAccessibilityInteractionConnectionCallback callback,
long threadId);
}
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java
index 4f3405b..5fee4de 100644
--- a/core/java/android/accounts/AccountManagerService.java
+++ b/core/java/android/accounts/AccountManagerService.java
@@ -62,6 +62,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -275,7 +276,7 @@ public class AccountManagerService
accountNames.add(accountName);
}
}
- for (HashMap.Entry<String, ArrayList<String>> cur
+ for (Map.Entry<String, ArrayList<String>> cur
: accountNamesByType.entrySet()) {
final String accountType = cur.getKey();
final ArrayList<String> accountNames = cur.getValue();
diff --git a/core/java/android/accounts/ChooseTypeAndAccountActivity.java b/core/java/android/accounts/ChooseTypeAndAccountActivity.java
index c3c9d16..136c68c 100644
--- a/core/java/android/accounts/ChooseTypeAndAccountActivity.java
+++ b/core/java/android/accounts/ChooseTypeAndAccountActivity.java
@@ -216,7 +216,7 @@ public class ChooseTypeAndAccountActivity extends Activity
if (mPendingRequest == REQUEST_NULL) {
// If there are no allowable accounts go directly to add account
- if (mAccountInfos.isEmpty()) {
+ if (shouldSkipToChooseAccountTypeFlow()) {
startChooseAccountTypeActivity();
return;
}
@@ -265,6 +265,12 @@ public class ChooseTypeAndAccountActivity extends Activity
mPendingRequest = REQUEST_NULL;
if (resultCode == RESULT_CANCELED) {
+ // if cancelling out of addAccount and the original state caused us to skip this,
+ // finish this activity
+ if (shouldSkipToChooseAccountTypeFlow()) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
return;
}
@@ -318,6 +324,14 @@ public class ChooseTypeAndAccountActivity extends Activity
finish();
}
+ /**
+ * convenience method to check if we should skip the accounts list display and immediately
+ * jump to the flow that asks the user to select from the account type list
+ */
+ private boolean shouldSkipToChooseAccountTypeFlow() {
+ return mAccountInfos.isEmpty();
+ }
+
protected void runAddAccountForAuthenticator(String type) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "runAddAccountForAuthenticator: " + type);
diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java
index 7f0ea99..274a9d5 100644
--- a/core/java/android/animation/LayoutTransition.java
+++ b/core/java/android/animation/LayoutTransition.java
@@ -321,13 +321,13 @@ public class LayoutTransition {
public long getStartDelay(int transitionType) {
switch (transitionType) {
case CHANGE_APPEARING:
- return mChangingAppearingDuration;
+ return mChangingAppearingDelay;
case CHANGE_DISAPPEARING:
- return mChangingDisappearingDuration;
+ return mChangingDisappearingDelay;
case APPEARING:
- return mAppearingDuration;
+ return mAppearingDelay;
case DISAPPEARING:
- return mDisappearingDuration;
+ return mDisappearingDelay;
}
// shouldn't reach here
return 0;
@@ -1024,18 +1024,25 @@ public class LayoutTransition {
*
* @param parent The ViewGroup to which the View is being added.
* @param child The View being added to the ViewGroup.
+ * @param changesLayout Whether the removal will cause changes in the layout of other views
+ * in the container. INVISIBLE views becoming VISIBLE will not cause changes and thus will not
+ * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations.
*/
- public void addChild(ViewGroup parent, View child) {
+ private void addChild(ViewGroup parent, View child, boolean changesLayout) {
// Want disappearing animations to finish up before proceeding
cancel(DISAPPEARING);
- // Also, cancel changing animations so that we start fresh ones from current locations
- cancel(CHANGE_APPEARING);
+ if (changesLayout) {
+ // Also, cancel changing animations so that we start fresh ones from current locations
+ cancel(CHANGE_APPEARING);
+ }
if (mListeners != null) {
for (TransitionListener listener : mListeners) {
listener.startTransition(this, parent, child, APPEARING);
}
}
- runChangeTransition(parent, child, APPEARING);
+ if (changesLayout) {
+ runChangeTransition(parent, child, APPEARING);
+ }
runAppearingTransition(parent, child);
}
@@ -1048,8 +1055,31 @@ public class LayoutTransition {
* @param parent The ViewGroup to which the View is being added.
* @param child The View being added to the ViewGroup.
*/
+ public void addChild(ViewGroup parent, View child) {
+ addChild(parent, child, true);
+ }
+
+ /**
+ * @deprecated Use {@link #showChild(android.view.ViewGroup, android.view.View, int)}.
+ */
+ @Deprecated
public void showChild(ViewGroup parent, View child) {
- addChild(parent, child);
+ addChild(parent, child, true);
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be made visible in the
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The ViewGroup in which the View is being made visible.
+ * @param child The View being made visible.
+ * @param oldVisibility The previous visibility value of the child View, either
+ * {@link View#GONE} or {@link View#INVISIBLE}.
+ */
+ public void showChild(ViewGroup parent, View child, int oldVisibility) {
+ addChild(parent, child, oldVisibility == View.GONE);
}
/**
@@ -1060,18 +1090,25 @@ public class LayoutTransition {
*
* @param parent The ViewGroup from which the View is being removed.
* @param child The View being removed from the ViewGroup.
+ * @param changesLayout Whether the removal will cause changes in the layout of other views
+ * in the container. Views becoming INVISIBLE will not cause changes and thus will not
+ * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations.
*/
- public void removeChild(ViewGroup parent, View child) {
+ private void removeChild(ViewGroup parent, View child, boolean changesLayout) {
// Want appearing animations to finish up before proceeding
cancel(APPEARING);
- // Also, cancel changing animations so that we start fresh ones from current locations
- cancel(CHANGE_DISAPPEARING);
+ if (changesLayout) {
+ // Also, cancel changing animations so that we start fresh ones from current locations
+ cancel(CHANGE_DISAPPEARING);
+ }
if (mListeners != null) {
for (TransitionListener listener : mListeners) {
listener.startTransition(this, parent, child, DISAPPEARING);
}
}
- runChangeTransition(parent, child, DISAPPEARING);
+ if (changesLayout) {
+ runChangeTransition(parent, child, DISAPPEARING);
+ }
runDisappearingTransition(parent, child);
}
@@ -1084,8 +1121,31 @@ public class LayoutTransition {
* @param parent The ViewGroup from which the View is being removed.
* @param child The View being removed from the ViewGroup.
*/
+ public void removeChild(ViewGroup parent, View child) {
+ removeChild(parent, child, true);
+ }
+
+ /**
+ * @deprecated Use {@link #hideChild(android.view.ViewGroup, android.view.View, int)}.
+ */
+ @Deprecated
public void hideChild(ViewGroup parent, View child) {
- removeChild(parent, child);
+ removeChild(parent, child, true);
+ }
+
+ /**
+ * This method is called by ViewGroup when a child view is about to be hidden in
+ * container. This callback starts the process of a transition; we grab the starting
+ * values, listen for changes to all of the children of the container, and start appropriate
+ * animations.
+ *
+ * @param parent The parent ViewGroup of the View being hidden.
+ * @param child The View being hidden.
+ * @param newVisibility The new visibility value of the child View, either
+ * {@link View#GONE} or {@link View#INVISIBLE}.
+ */
+ public void hideChild(ViewGroup parent, View child, int newVisibility) {
+ removeChild(parent, child, newVisibility == View.GONE);
}
/**
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 55e95b0..c7a129e 100755
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -20,6 +20,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AndroidRuntimeException;
+import android.view.Choreographer;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
@@ -52,17 +53,10 @@ public class ValueAnimator extends Animator {
* Internal constants
*/
- /*
- * The default amount of time in ms between animation frames
- */
- private static final long DEFAULT_FRAME_DELAY = 10;
-
/**
- * 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
+ * Messages sent to timing handler: START is sent when an animation first begins.
*/
static final int ANIMATION_START = 0;
- static final int ANIMATION_FRAME = 1;
/**
* Values used with internal variable mPlayingState to indicate the current state of an
@@ -90,70 +84,15 @@ public class ValueAnimator extends Animator {
*/
long mSeekTime = -1;
- // TODO: We access the following ThreadLocal variables often, some of them on every update.
- // If ThreadLocal access is significantly expensive, we may want to put all of these
- // fields into a structure sot hat we just access ThreadLocal once to get the reference
- // to that structure, then access the structure directly for each field.
-
// The static sAnimationHandler processes the internal timing loop on which all animations
// are based
private static ThreadLocal<AnimationHandler> sAnimationHandler =
new ThreadLocal<AnimationHandler>();
- // The per-thread list of all active animations
- private static final ThreadLocal<ArrayList<ValueAnimator>> sAnimations =
- new ThreadLocal<ArrayList<ValueAnimator>>() {
- @Override
- protected ArrayList<ValueAnimator> initialValue() {
- return new ArrayList<ValueAnimator>();
- }
- };
-
- // The per-thread set of animations to be started on the next animation frame
- private static final ThreadLocal<ArrayList<ValueAnimator>> sPendingAnimations =
- new ThreadLocal<ArrayList<ValueAnimator>>() {
- @Override
- protected ArrayList<ValueAnimator> initialValue() {
- return new ArrayList<ValueAnimator>();
- }
- };
-
- /**
- * Internal per-thread collections used to avoid set collisions as animations start and end
- * while being processed.
- */
- private static final ThreadLocal<ArrayList<ValueAnimator>> sDelayedAnims =
- new ThreadLocal<ArrayList<ValueAnimator>>() {
- @Override
- protected ArrayList<ValueAnimator> initialValue() {
- return new ArrayList<ValueAnimator>();
- }
- };
-
- private static final ThreadLocal<ArrayList<ValueAnimator>> sEndingAnims =
- new ThreadLocal<ArrayList<ValueAnimator>>() {
- @Override
- protected ArrayList<ValueAnimator> initialValue() {
- return new ArrayList<ValueAnimator>();
- }
- };
-
- private static final ThreadLocal<ArrayList<ValueAnimator>> sReadyAnims =
- new ThreadLocal<ArrayList<ValueAnimator>>() {
- @Override
- protected ArrayList<ValueAnimator> initialValue() {
- return new ArrayList<ValueAnimator>();
- }
- };
-
// The time interpolator to be used if none is set on the animation
private static final TimeInterpolator sDefaultInterpolator =
new AccelerateDecelerateInterpolator();
- // type evaluators for the primitive types handled by this implementation
- private static final TypeEvaluator sIntEvaluator = new IntEvaluator();
- private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator();
-
/**
* Used to indicate whether the animation is currently playing in reverse. This causes the
* elapsed fraction to be inverted to calculate the appropriate values.
@@ -224,9 +163,6 @@ public class ValueAnimator extends Animator {
// 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;
@@ -573,119 +509,146 @@ public class ValueAnimator extends Animator {
* animations possible.
*
*/
- private static class AnimationHandler extends Handler {
+ private static class AnimationHandler extends Handler
+ implements Choreographer.OnAnimateListener {
+ // The per-thread list of all active animations
+ private final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
+
+ // The per-thread set of animations to be started on the next animation frame
+ private final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();
+
/**
- * 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
+ * Internal per-thread collections used to avoid set collisions as animations start and end
+ * while being processed.
+ */
+ private final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();
+ private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();
+ private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();
+
+ private final Choreographer mChoreographer;
+ private boolean mIsChoreographed;
+
+ private AnimationHandler() {
+ mChoreographer = Choreographer.getInstance();
+ }
+
+ /**
+ * 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;
- ArrayList<ValueAnimator> animations = sAnimations.get();
- ArrayList<ValueAnimator> delayedAnims = sDelayedAnims.get();
switch (msg.what) {
- // TODO: should we avoid sending frame message when starting if we
- // were already running?
case ANIMATION_START:
- ArrayList<ValueAnimator> pendingAnimations = sPendingAnimations.get();
- if (animations.size() > 0 || delayedAnims.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 (pendingAnimations.size() > 0) {
- ArrayList<ValueAnimator> pendingCopy =
- (ArrayList<ValueAnimator>) pendingAnimations.clone();
- pendingAnimations.clear();
- int count = pendingCopy.size();
- for (int i = 0; i < count; ++i) {
- ValueAnimator anim = pendingCopy.get(i);
- // If the animation has a startDelay, place it on the delayed list
- if (anim.mStartDelay == 0) {
- anim.startAnimation();
- } else {
- delayedAnims.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();
- ArrayList<ValueAnimator> readyAnims = sReadyAnims.get();
- ArrayList<ValueAnimator> endingAnims = sEndingAnims.get();
-
- // First, process animations currently sitting on the delayed queue, adding
- // them to the active animations if they are ready
- int numDelayedAnims = delayedAnims.size();
- for (int i = 0; i < numDelayedAnims; ++i) {
- ValueAnimator anim = delayedAnims.get(i);
- if (anim.delayedAnimationFrame(currentTime)) {
- readyAnims.add(anim);
- }
- }
- int numReadyAnims = readyAnims.size();
- if (numReadyAnims > 0) {
- for (int i = 0; i < numReadyAnims; ++i) {
- ValueAnimator anim = readyAnims.get(i);
- anim.startAnimation();
- anim.mRunning = true;
- delayedAnims.remove(anim);
- }
- readyAnims.clear();
- }
+ doAnimationStart();
+ break;
+ }
+ }
- // Now process all active animations. The return value from animationFrame()
- // tells the handler whether it should now be ended
- int numAnims = animations.size();
- int i = 0;
- while (i < numAnims) {
- ValueAnimator anim = animations.get(i);
- if (anim.animationFrame(currentTime)) {
- endingAnims.add(anim);
- }
- if (animations.size() == numAnims) {
- ++i;
- } else {
- // An animation might be canceled or ended by client code
- // during the animation frame. Check to see if this happened by
- // seeing whether the current index is the same as it was before
- // calling animationFrame(). Another approach would be to copy
- // animations to a temporary list and process that list instead,
- // but that entails garbage and processing overhead that would
- // be nice to avoid.
- --numAnims;
- endingAnims.remove(anim);
- }
- }
- if (endingAnims.size() > 0) {
- for (i = 0; i < endingAnims.size(); ++i) {
- endingAnims.get(i).endAnimation();
- }
- endingAnims.clear();
+ private void doAnimationStart() {
+ // mPendingAnimations holds any animations that have requested to be started
+ // We're going to clear mPendingAnimations, 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 mPendingAnimations
+ // is empty.
+ while (mPendingAnimations.size() > 0) {
+ ArrayList<ValueAnimator> pendingCopy =
+ (ArrayList<ValueAnimator>) mPendingAnimations.clone();
+ mPendingAnimations.clear();
+ int count = pendingCopy.size();
+ for (int i = 0; i < count; ++i) {
+ ValueAnimator anim = pendingCopy.get(i);
+ // If the animation has a startDelay, place it on the delayed list
+ if (anim.mStartDelay == 0) {
+ anim.startAnimation(this);
+ } else {
+ mDelayedAnims.add(anim);
}
+ }
+ }
+ doAnimationFrame();
+ }
- // If there are still active or delayed animations, call the handler again
- // after the frameDelay
- if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) {
- sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -
- (AnimationUtils.currentAnimationTimeMillis() - currentTime)));
- }
- break;
+ private void doAnimationFrame() {
+ // 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 = mDelayedAnims.size();
+ for (int i = 0; i < numDelayedAnims; ++i) {
+ ValueAnimator anim = mDelayedAnims.get(i);
+ if (anim.delayedAnimationFrame(currentTime)) {
+ mReadyAnims.add(anim);
+ }
+ }
+ int numReadyAnims = mReadyAnims.size();
+ if (numReadyAnims > 0) {
+ for (int i = 0; i < numReadyAnims; ++i) {
+ ValueAnimator anim = mReadyAnims.get(i);
+ anim.startAnimation(this);
+ anim.mRunning = true;
+ mDelayedAnims.remove(anim);
+ }
+ mReadyAnims.clear();
+ }
+
+ // Now process all active animations. The return value from animationFrame()
+ // tells the handler whether it should now be ended
+ int numAnims = mAnimations.size();
+ int i = 0;
+ while (i < numAnims) {
+ ValueAnimator anim = mAnimations.get(i);
+ if (anim.animationFrame(currentTime)) {
+ mEndingAnims.add(anim);
+ }
+ if (mAnimations.size() == numAnims) {
+ ++i;
+ } else {
+ // An animation might be canceled or ended by client code
+ // during the animation frame. Check to see if this happened by
+ // seeing whether the current index is the same as it was before
+ // calling animationFrame(). Another approach would be to copy
+ // animations to a temporary list and process that list instead,
+ // but that entails garbage and processing overhead that would
+ // be nice to avoid.
+ --numAnims;
+ mEndingAnims.remove(anim);
+ }
+ }
+ if (mEndingAnims.size() > 0) {
+ for (i = 0; i < mEndingAnims.size(); ++i) {
+ mEndingAnims.get(i).endAnimation(this);
+ }
+ mEndingAnims.clear();
+ }
+
+ // If there are still active or delayed animations, schedule a future call to
+ // onAnimate to process the next frame of the animations.
+ if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
+ if (!mIsChoreographed) {
+ mIsChoreographed = true;
+ mChoreographer.addOnAnimateListener(this);
+ }
+ mChoreographer.scheduleAnimation();
+ } else {
+ if (mIsChoreographed) {
+ mIsChoreographed = false;
+ mChoreographer.removeOnAnimateListener(this);
+ }
}
}
+
+ @Override
+ public void onAnimate() {
+ doAnimationFrame();
+ }
}
/**
@@ -715,10 +678,13 @@ public class ValueAnimator extends Animator {
* function because the same delay will be applied to all animations, since they are all
* run off of a single timing loop.
*
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ *
* @return the requested time between frames, in milliseconds
*/
public static long getFrameDelay() {
- return sFrameDelay;
+ return Choreographer.getFrameDelay();
}
/**
@@ -728,10 +694,13 @@ public class ValueAnimator extends Animator {
* function because the same delay will be applied to all animations, since they are all
* run off of a single timing loop.
*
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ *
* @param frameDelay the requested time between frames, in milliseconds
*/
public static void setFrameDelay(long frameDelay) {
- sFrameDelay = frameDelay;
+ Choreographer.setFrameDelay(frameDelay);
}
/**
@@ -928,7 +897,8 @@ public class ValueAnimator extends Animator {
mPlayingState = STOPPED;
mStarted = true;
mStartedDelay = false;
- sPendingAnimations.get().add(this);
+ AnimationHandler animationHandler = getOrCreateAnimationHandler();
+ animationHandler.mPendingAnimations.add(this);
if (mStartDelay == 0) {
// This sets the initial value of the animation, prior to actually starting it running
setCurrentPlayTime(getCurrentPlayTime());
@@ -944,11 +914,6 @@ public class ValueAnimator extends Animator {
}
}
}
- AnimationHandler animationHandler = sAnimationHandler.get();
- if (animationHandler == null) {
- animationHandler = new AnimationHandler();
- sAnimationHandler.set(animationHandler);
- }
animationHandler.sendEmptyMessage(ANIMATION_START);
}
@@ -961,8 +926,10 @@ public class ValueAnimator extends Animator {
public void cancel() {
// Only cancel if the animation is actually running or has been started and is about
// to run
- if (mPlayingState != STOPPED || sPendingAnimations.get().contains(this) ||
- sDelayedAnims.get().contains(this)) {
+ AnimationHandler handler = getOrCreateAnimationHandler();
+ if (mPlayingState != STOPPED
+ || handler.mPendingAnimations.contains(this)
+ || handler.mDelayedAnims.contains(this)) {
// Only notify listeners if the animator has actually started
if (mRunning && mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
@@ -971,16 +938,17 @@ public class ValueAnimator extends Animator {
listener.onAnimationCancel(this);
}
}
- endAnimation();
+ endAnimation(handler);
}
}
@Override
public void end() {
- if (!sAnimations.get().contains(this) && !sPendingAnimations.get().contains(this)) {
+ AnimationHandler handler = getOrCreateAnimationHandler();
+ if (!handler.mAnimations.contains(this) && !handler.mPendingAnimations.contains(this)) {
// Special case if the animation has not yet started; get it ready for ending
mStartedDelay = false;
- startAnimation();
+ startAnimation(handler);
} else if (!mInitialized) {
initAnimation();
}
@@ -991,7 +959,7 @@ public class ValueAnimator extends Animator {
} else {
animateValue(1f);
}
- endAnimation();
+ endAnimation(handler);
}
@Override
@@ -1027,10 +995,10 @@ public class ValueAnimator extends Animator {
* Called internally to end an animation by removing it from the animations list. Must be
* called on the UI thread.
*/
- private void endAnimation() {
- sAnimations.get().remove(this);
- sPendingAnimations.get().remove(this);
- sDelayedAnims.get().remove(this);
+ private void endAnimation(AnimationHandler handler) {
+ handler.mAnimations.remove(this);
+ handler.mPendingAnimations.remove(this);
+ handler.mDelayedAnims.remove(this);
mPlayingState = STOPPED;
if (mRunning && mListeners != null) {
ArrayList<AnimatorListener> tmpListeners =
@@ -1048,9 +1016,9 @@ public class ValueAnimator extends Animator {
* Called internally to start an animation by adding it to the active animations list. Must be
* called on the UI thread.
*/
- private void startAnimation() {
+ private void startAnimation(AnimationHandler handler) {
initAnimation();
- sAnimations.get().add(this);
+ handler.mAnimations.add(this);
if (mStartDelay > 0 && mListeners != null) {
// Listeners were already notified in start() if startDelay is 0; this is
// just for delayed animations
@@ -1236,13 +1204,14 @@ public class ValueAnimator extends Animator {
/**
* Return the number of animations currently running.
*
- * Used by StrictMode internally to annotate violations. Only
- * called on the main thread.
+ * Used by StrictMode internally to annotate violations.
+ * May be called on arbitrary threads!
*
* @hide
*/
public static int getCurrentAnimationsCount() {
- return sAnimations.get().size();
+ AnimationHandler handler = sAnimationHandler.get();
+ return handler != null ? handler.mAnimations.size() : 0;
}
/**
@@ -1252,9 +1221,21 @@ public class ValueAnimator extends Animator {
* @hide
*/
public static void clearAllAnimations() {
- sAnimations.get().clear();
- sPendingAnimations.get().clear();
- sDelayedAnims.get().clear();
+ AnimationHandler handler = sAnimationHandler.get();
+ if (handler != null) {
+ handler.mAnimations.clear();
+ handler.mPendingAnimations.clear();
+ handler.mDelayedAnims.clear();
+ }
+ }
+
+ private AnimationHandler getOrCreateAnimationHandler() {
+ AnimationHandler handler = sAnimationHandler.get();
+ if (handler == null) {
+ handler = new AnimationHandler();
+ sAnimationHandler.set(handler);
+ }
+ return handler;
}
@Override
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 4fe9cef..9661b9e 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1442,9 +1442,10 @@ public class ActivityManager {
public int getLauncherLargeIconDensity() {
final Resources res = mContext.getResources();
final int density = res.getDisplayMetrics().densityDpi;
+ final int sw = res.getConfiguration().smallestScreenWidthDp;
- if ((res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
- != Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ if (sw < 600) {
+ // Smaller than approx 7" tablets, use the regular icon size.
return density;
}
@@ -1456,9 +1457,13 @@ public class ActivityManager {
case DisplayMetrics.DENSITY_HIGH:
return DisplayMetrics.DENSITY_XHIGH;
case DisplayMetrics.DENSITY_XHIGH:
- return DisplayMetrics.DENSITY_MEDIUM * 2;
+ return DisplayMetrics.DENSITY_XXHIGH;
+ case DisplayMetrics.DENSITY_XXHIGH:
+ return DisplayMetrics.DENSITY_XHIGH * 2;
default:
- return density;
+ // The density is some abnormal value. Return some other
+ // abnormal value that is a reasonable scaling of it.
+ return (int)(density*1.5f);
}
}
@@ -1471,9 +1476,10 @@ public class ActivityManager {
public int getLauncherLargeIconSize() {
final Resources res = mContext.getResources();
final int size = res.getDimensionPixelSize(android.R.dimen.app_icon_size);
+ final int sw = res.getConfiguration().smallestScreenWidthDp;
- if ((res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
- != Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ if (sw < 600) {
+ // Smaller than approx 7" tablets, use the regular icon size.
return size;
}
@@ -1487,9 +1493,13 @@ public class ActivityManager {
case DisplayMetrics.DENSITY_HIGH:
return (size * DisplayMetrics.DENSITY_XHIGH) / DisplayMetrics.DENSITY_HIGH;
case DisplayMetrics.DENSITY_XHIGH:
- return (size * DisplayMetrics.DENSITY_MEDIUM * 2) / DisplayMetrics.DENSITY_XHIGH;
+ return (size * DisplayMetrics.DENSITY_XXHIGH) / DisplayMetrics.DENSITY_XHIGH;
+ case DisplayMetrics.DENSITY_XXHIGH:
+ return (size * DisplayMetrics.DENSITY_XHIGH*2) / DisplayMetrics.DENSITY_XXHIGH;
default:
- return size;
+ // The density is some abnormal value. Return some other
+ // abnormal value that is a reasonable scaling of it.
+ return (int)(size*1.5f);
}
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 0c761fc..3c5f53a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -65,6 +65,7 @@ import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
import android.util.LogPrinter;
+import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.view.Display;
import android.view.HardwareRenderer;
@@ -485,7 +486,6 @@ public final class ActivityThread {
private static final String HEAP_COLUMN = "%13s %8s %8s %8s %8s %8s %8s";
private static final String ONE_COUNT_COLUMN = "%21s %8d";
private static final String TWO_COUNT_COLUMNS = "%21s %8d %21s %8d";
- private static final String TWO_COUNT_COLUMNS_DB = "%21s %8d %21s %8d";
private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s";
// Formatting for checkin service - update version if row format changes
@@ -813,6 +813,19 @@ public final class ActivityThread {
}
}
+ public void dumpProvider(FileDescriptor fd, IBinder providertoken,
+ String[] args) {
+ DumpComponentInfo data = new DumpComponentInfo();
+ try {
+ data.fd = ParcelFileDescriptor.dup(fd);
+ data.token = providertoken;
+ data.args = args;
+ queueOrSendMessage(H.DUMP_PROVIDER, data);
+ } catch (IOException e) {
+ Slog.w(TAG, "dumpProvider failed", e);
+ }
+ }
+
@Override
public Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, boolean checkin,
boolean all, String[] args) {
@@ -853,7 +866,6 @@ public final class ActivityThread {
int binderProxyObjectCount = Debug.getBinderProxyObjectCount();
int binderDeathObjectCount = Debug.getBinderDeathObjectCount();
long openSslSocketCount = Debug.countInstancesOfClass(OpenSSLSocketImpl.class);
- long sqliteAllocated = SQLiteDebug.getHeapAllocatedSize() / 1024;
SQLiteDebug.PagerStats stats = SQLiteDebug.getDatabaseInfo();
// For checkin, we print one long comma-separated list of values
@@ -921,9 +933,9 @@ public final class ActivityThread {
pw.print(openSslSocketCount); pw.print(',');
// SQL
- pw.print(sqliteAllocated); pw.print(',');
pw.print(stats.memoryUsed / 1024); pw.print(',');
- pw.print(stats.pageCacheOverflo / 1024); pw.print(',');
+ pw.print(stats.memoryUsed / 1024); pw.print(',');
+ pw.print(stats.pageCacheOverflow / 1024); pw.print(',');
pw.print(stats.largestMemAlloc / 1024);
for (int i = 0; i < stats.dbStats.size(); i++) {
DbStats dbStats = stats.dbStats.get(i);
@@ -989,10 +1001,9 @@ public final class ActivityThread {
// SQLite mem info
pw.println(" ");
pw.println(" SQL");
- printRow(pw, TWO_COUNT_COLUMNS_DB, "heap:", sqliteAllocated, "MEMORY_USED:",
- stats.memoryUsed / 1024);
- printRow(pw, TWO_COUNT_COLUMNS_DB, "PAGECACHE_OVERFLOW:",
- stats.pageCacheOverflo / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024);
+ printRow(pw, ONE_COUNT_COLUMN, "MEMORY_USED:", stats.memoryUsed / 1024);
+ printRow(pw, TWO_COUNT_COLUMNS, "PAGECACHE_OVERFLOW:",
+ stats.pageCacheOverflow / 1024, "MALLOC_SIZE:", stats.largestMemAlloc / 1024);
pw.println(" ");
int N = stats.dbStats.size();
if (N > 0) {
@@ -1026,6 +1037,14 @@ public final class ActivityThread {
WindowManagerImpl.getDefault().dumpGfxInfo(fd);
}
+ @Override
+ public void dumpDbInfo(FileDescriptor fd, String[] args) {
+ PrintWriter pw = new PrintWriter(new FileOutputStream(fd));
+ PrintWriterPrinter printer = new PrintWriterPrinter(pw);
+ SQLiteDebug.dump(printer, args);
+ pw.flush();
+ }
+
private void printRow(PrintWriter pw, String format, Object...objs) {
pw.println(String.format(format, objs));
}
@@ -1044,6 +1063,7 @@ public final class ActivityThread {
public void scheduleTrimMemory(int level) {
queueOrSendMessage(H.TRIM_MEMORY, null, level);
}
+
}
private class H extends Handler {
@@ -1088,6 +1108,7 @@ public final class ActivityThread {
public static final int SET_CORE_SETTINGS = 138;
public static final int UPDATE_PACKAGE_COMPATIBILITY_INFO = 139;
public static final int TRIM_MEMORY = 140;
+ public static final int DUMP_PROVIDER = 141;
String codeToString(int code) {
if (DEBUG_MESSAGES) {
switch (code) {
@@ -1132,6 +1153,7 @@ public final class ActivityThread {
case SET_CORE_SETTINGS: return "SET_CORE_SETTINGS";
case UPDATE_PACKAGE_COMPATIBILITY_INFO: return "UPDATE_PACKAGE_COMPATIBILITY_INFO";
case TRIM_MEMORY: return "TRIM_MEMORY";
+ case DUMP_PROVIDER: return "DUMP_PROVIDER";
}
}
return "(unknown)";
@@ -1264,6 +1286,9 @@ public final class ActivityThread {
case DUMP_ACTIVITY:
handleDumpActivity((DumpComponentInfo)msg.obj);
break;
+ case DUMP_PROVIDER:
+ handleDumpProvider((DumpComponentInfo)msg.obj);
+ break;
case SLEEPING:
handleSleeping((IBinder)msg.obj, msg.arg1 != 0);
break;
@@ -2347,6 +2372,19 @@ public final class ActivityThread {
}
}
+ private void handleDumpProvider(DumpComponentInfo info) {
+ ProviderClientRecord r = mLocalProviders.get(info.token);
+ if (r != null && r.mLocalProvider != null) {
+ PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd.getFileDescriptor()));
+ r.mLocalProvider.dump(info.fd.getFileDescriptor(), pw, info.args);
+ pw.flush();
+ try {
+ info.fd.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
private void handleServiceArgs(ServiceArgsData data) {
Service s = mServices.get(data.token);
if (s != null) {
@@ -3703,7 +3741,6 @@ public final class ActivityThread {
}
final void handleTrimMemory(int level) {
- WindowManagerImpl.getDefault().trimMemory(level);
ArrayList<ComponentCallbacks2> callbacks;
synchronized (mPackages) {
@@ -3714,6 +3751,7 @@ public final class ActivityThread {
for (int i=0; i<N; i++) {
callbacks.get(i).onTrimMemory(level);
}
+ WindowManagerImpl.getDefault().trimMemory(level);
}
private void setupGraphicsSupport(LoadedApk info) {
@@ -3766,7 +3804,7 @@ public final class ActivityThread {
// implementation to use the pool executor. Normally, we use the
// serialized executor as the default. This has to happen in the
// main thread so the main looper is set right.
- if (data.appInfo.targetSdkVersion <= 12) {
+ if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {
AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@@ -4375,7 +4413,7 @@ public final class ActivityThread {
});
}
- public static final ActivityThread systemMain() {
+ public static ActivityThread systemMain() {
HardwareRenderer.disable(true);
ActivityThread thread = new ActivityThread();
thread.attach(true);
@@ -4416,6 +4454,8 @@ public final class ActivityThread {
ActivityThread thread = new ActivityThread();
thread.attach(false);
+ AsyncTask.init();
+
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java
index 9082003..2fe682d 100644
--- a/core/java/android/app/AlarmManager.java
+++ b/core/java/android/app/AlarmManager.java
@@ -16,10 +16,8 @@
package android.app;
-import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
-import android.os.ServiceManager;
/**
* This class provides access to the system alarm services. These allow you
@@ -117,8 +115,8 @@ public class AlarmManager
*
* @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC or
* RTC_WAKEUP.
- * @param triggerAtTime Time the alarm should go off, using the
- * appropriate clock (depending on the alarm type).
+ * @param triggerAtMillis time in milliseconds that the alarm should go
+ * off, using the appropriate clock (depending on the alarm type).
* @param operation Action to perform when the alarm goes off;
* typically comes from {@link PendingIntent#getBroadcast
* IntentSender.getBroadcast()}.
@@ -134,9 +132,9 @@ public class AlarmManager
* @see #RTC
* @see #RTC_WAKEUP
*/
- public void set(int type, long triggerAtTime, PendingIntent operation) {
+ public void set(int type, long triggerAtMillis, PendingIntent operation) {
try {
- mService.set(type, triggerAtTime, operation);
+ mService.set(type, triggerAtMillis, operation);
} catch (RemoteException ex) {
}
}
@@ -169,9 +167,10 @@ public class AlarmManager
*
* @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP}, RTC or
* RTC_WAKEUP.
- * @param triggerAtTime Time the alarm should first go off, using the
- * appropriate clock (depending on the alarm type).
- * @param interval Interval between subsequent repeats of the alarm.
+ * @param triggerAtMillis time in milliseconds that the alarm should first
+ * go off, using the appropriate clock (depending on the alarm type).
+ * @param intervalMillis interval in milliseconds between subsequent repeats
+ * of the alarm.
* @param operation Action to perform when the alarm goes off;
* typically comes from {@link PendingIntent#getBroadcast
* IntentSender.getBroadcast()}.
@@ -187,10 +186,10 @@ public class AlarmManager
* @see #RTC
* @see #RTC_WAKEUP
*/
- public void setRepeating(int type, long triggerAtTime, long interval,
- PendingIntent operation) {
+ public void setRepeating(int type, long triggerAtMillis,
+ long intervalMillis, PendingIntent operation) {
try {
- mService.setRepeating(type, triggerAtTime, interval, operation);
+ mService.setRepeating(type, triggerAtMillis, intervalMillis, operation);
} catch (RemoteException ex) {
}
}
@@ -219,20 +218,20 @@ public class AlarmManager
* requested, the time between any two successive firings of the alarm
* may vary. If your application demands very low jitter, use
* {@link #setRepeating} instead.
- *
+ *
* @param type One of ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP}, RTC or
* RTC_WAKEUP.
- * @param triggerAtTime Time the alarm should first go off, using the
- * appropriate clock (depending on the alarm type). This
- * is inexact: the alarm will not fire before this time,
- * but there may be a delay of almost an entire alarm
- * interval before the first invocation of the alarm.
- * @param interval Interval between subsequent repeats of the alarm. If
- * this is one of INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR,
- * INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY then the
- * alarm will be phase-aligned with other alarms to reduce
- * the number of wakeups. Otherwise, the alarm will be set
- * as though the application had called {@link #setRepeating}.
+ * @param triggerAtMillis time in milliseconds that the alarm should first
+ * go off, using the appropriate clock (depending on the alarm type). This
+ * is inexact: the alarm will not fire before this time, but there may be a
+ * delay of almost an entire alarm interval before the first invocation of
+ * the alarm.
+ * @param intervalMillis interval in milliseconds between subsequent repeats
+ * of the alarm. If this is one of INTERVAL_FIFTEEN_MINUTES,
+ * INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_HALF_DAY, or INTERVAL_DAY
+ * then the alarm will be phase-aligned with other alarms to reduce the
+ * number of wakeups. Otherwise, the alarm will be set as though the
+ * application had called {@link #setRepeating}.
* @param operation Action to perform when the alarm goes off;
* typically comes from {@link PendingIntent#getBroadcast
* IntentSender.getBroadcast()}.
@@ -253,10 +252,10 @@ public class AlarmManager
* @see #INTERVAL_HALF_DAY
* @see #INTERVAL_DAY
*/
- public void setInexactRepeating(int type, long triggerAtTime, long interval,
- PendingIntent operation) {
+ public void setInexactRepeating(int type, long triggerAtMillis,
+ long intervalMillis, PendingIntent operation) {
try {
- mService.setInexactRepeating(type, triggerAtTime, interval, operation);
+ mService.setInexactRepeating(type, triggerAtMillis, intervalMillis, operation);
} catch (RemoteException ex) {
}
}
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
index c4a4fea..e75d7b4 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -352,6 +352,21 @@ public abstract class ApplicationThreadNative extends Binder
return true;
}
+ case DUMP_PROVIDER_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ ParcelFileDescriptor fd = data.readFileDescriptor();
+ final IBinder service = data.readStrongBinder();
+ final String[] args = data.readStringArray();
+ if (fd != null) {
+ dumpProvider(fd.getFileDescriptor(), service, args);
+ try {
+ fd.close();
+ } catch (IOException e) {
+ }
+ }
+ return true;
+ }
+
case SCHEDULE_REGISTERED_RECEIVER_TRANSACTION: {
data.enforceInterface(IApplicationThread.descriptor);
IIntentReceiver receiver = IIntentReceiver.Stub.asInterface(
@@ -539,6 +554,26 @@ public abstract class ApplicationThreadNative extends Binder
reply.writeNoException();
return true;
}
+
+ case DUMP_DB_INFO_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ ParcelFileDescriptor fd = data.readFileDescriptor();
+ String[] args = data.readStringArray();
+ if (fd != null) {
+ try {
+ dumpDbInfo(fd.getFileDescriptor(), args);
+ } finally {
+ try {
+ fd.close();
+ } catch (IOException e) {
+ // swallowed, not propagated back to the caller
+ }
+ }
+ }
+ reply.writeNoException();
+ return true;
+ }
}
return super.onTransact(code, data, reply, flags);
@@ -931,6 +966,17 @@ class ApplicationThreadProxy implements IApplicationThread {
data.recycle();
}
+ public void dumpProvider(FileDescriptor fd, IBinder token, String[] args)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeFileDescriptor(fd);
+ data.writeStrongBinder(token);
+ data.writeStringArray(args);
+ mRemote.transact(DUMP_PROVIDER_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
int resultCode, String dataStr, Bundle extras, boolean ordered, boolean sticky)
throws RemoteException {
@@ -1105,4 +1151,13 @@ class ApplicationThreadProxy implements IApplicationThread {
mRemote.transact(DUMP_GFX_INFO_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
data.recycle();
}
+
+ public void dumpDbInfo(FileDescriptor fd, String[] args) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeFileDescriptor(fd);
+ data.writeStringArray(args);
+ mRemote.transact(DUMP_DB_INFO_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
}
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
index 1253fe7..6ad1736 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -102,6 +102,8 @@ public interface IApplicationThread extends IInterface {
void processInBackground() throws RemoteException;
void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args)
throws RemoteException;
+ void dumpProvider(FileDescriptor fd, IBinder servicetoken, String[] args)
+ throws RemoteException;
void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
int resultCode, String data, Bundle extras, boolean ordered, boolean sticky)
throws RemoteException;
@@ -125,6 +127,7 @@ public interface IApplicationThread extends IInterface {
Debug.MemoryInfo dumpMemInfo(FileDescriptor fd, boolean checkin, boolean all,
String[] args) throws RemoteException;
void dumpGfxInfo(FileDescriptor fd, String[] args) throws RemoteException;
+ void dumpDbInfo(FileDescriptor fd, String[] args) throws RemoteException;
String descriptor = "android.app.IApplicationThread";
@@ -171,4 +174,6 @@ public interface IApplicationThread extends IInterface {
int SCHEDULE_TRIM_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+41;
int DUMP_MEM_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+42;
int DUMP_GFX_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+43;
+ int DUMP_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+44;
+ int DUMP_DB_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+45;
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index d7f5c55..c037ffb 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -24,14 +24,14 @@ import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Bundle;
-import android.os.PerformanceCollector;
-import android.os.RemoteException;
import android.os.Debug;
import android.os.IBinder;
import android.os.MessageQueue;
+import android.os.PerformanceCollector;
import android.os.Process;
-import android.os.SystemClock;
+import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.SystemClock;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.IWindowManager;
@@ -40,7 +40,6 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.Window;
-import android.view.inputmethod.InputMethodManager;
import java.io.File;
import java.util.ArrayList;
@@ -834,16 +833,21 @@ public class Instrumentation {
return;
}
KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
-
+
KeyEvent[] events = keyCharacterMap.getEvents(text.toCharArray());
-
+
if (events != null) {
for (int i = 0; i < events.length; i++) {
- sendKeySync(events[i]);
+ // We have to change the time of an event before injecting it because
+ // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
+ // time stamp and the system rejects too old events. Hence, it is
+ // possible for an event to become stale before it is injected if it
+ // takes too long to inject the preceding ones.
+ sendKeySync(KeyEvent.changeTimeRepeat(events[i], SystemClock.uptimeMillis(), 0));
}
- }
+ }
}
-
+
/**
* Send a key event to the currently focused window/view and wait for it to
* be processed. Finished at some point after the recipient has returned
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
index 8fa95b4..d04e9db 100644
--- a/core/java/android/app/SearchDialog.java
+++ b/core/java/android/app/SearchDialog.java
@@ -551,7 +551,6 @@ public class SearchDialog extends Dialog {
try {
// If the intent was created from a suggestion, it will always have an explicit
// component here.
- Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toUri(0));
getContext().startActivity(intent);
// If the search switches to a different activity,
// SearchDialogWrapper#performActivityResuming
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 5b8addf..dd9f337 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -56,6 +56,11 @@ public class StatusBarManager {
| DISABLE_NOTIFICATION_ALERTS | DISABLE_NOTIFICATION_TICKER
| DISABLE_SYSTEM_INFO | DISABLE_RECENT | DISABLE_HOME | DISABLE_BACK | DISABLE_CLOCK;
+ public static final int NAVIGATION_HINT_BACK_NOP = 1 << 0;
+ public static final int NAVIGATION_HINT_HOME_NOP = 1 << 1;
+ public static final int NAVIGATION_HINT_RECENT_NOP = 1 << 2;
+ public static final int NAVIGATION_HINT_BACK_ALT = 1 << 3;
+
private Context mContext;
private IStatusBarService mService;
private IBinder mToken = new Binder();
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 71f6445..0c22740 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -168,7 +168,7 @@ public class UiModeManager {
* {@link Configuration#UI_MODE_TYPE_NORMAL Configuration.UI_MODE_TYPE_NORMAL},
* {@link Configuration#UI_MODE_TYPE_DESK Configuration.UI_MODE_TYPE_DESK}, or
* {@link Configuration#UI_MODE_TYPE_CAR Configuration.UI_MODE_TYPE_CAR}, or
- * {@link Configuration#UI_MODE_TYPE_TELEVISION Configuration.UI_MODE_TYPE_TV}.
+ * {@link Configuration#UI_MODE_TYPE_TELEVISION Configuration.UI_MODE_TYPE_APPLIANCE}.
*/
public int getCurrentModeType() {
if (mService != null) {
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index b1c1f30..c1e28b0 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -213,10 +213,6 @@ public class WallpaperManager {
mHandler.sendEmptyMessage(MSG_CLEAR_WALLPAPER);
}
- public Handler getHandler() {
- return mHandler;
- }
-
public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) {
synchronized (this) {
if (mWallpaper != null) {
@@ -623,24 +619,14 @@ public class WallpaperManager {
* @param yOffset The offset along the Y dimension, from 0 to 1.
*/
public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) {
- final IBinder fWindowToken = windowToken;
- final float fXOffset = xOffset;
- final float fYOffset = yOffset;
- sGlobals.getHandler().post(new Runnable() {
- public void run() {
- try {
- //Log.v(TAG, "Sending new wallpaper offsets from app...");
- ViewRootImpl.getWindowSession(mContext.getMainLooper()).setWallpaperPosition(
- fWindowToken, fXOffset, fYOffset, mWallpaperXStep, mWallpaperYStep);
- //Log.v(TAG, "...app returning after sending offsets!");
- } catch (RemoteException e) {
- // Ignore.
- } catch (IllegalArgumentException e) {
- // Since this is being posted, it's possible that this windowToken is no longer
- // valid, for example, if setWallpaperOffsets is called just before rotation.
- }
- }
- });
+ try {
+ //Log.v(TAG, "Sending new wallpaper offsets from app...");
+ ViewRootImpl.getWindowSession(mContext.getMainLooper()).setWallpaperPosition(
+ windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep);
+ //Log.v(TAG, "...app returning after sending offsets!");
+ } catch (RemoteException e) {
+ // Ignore.
+ }
}
/**
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index 899816c..600ce6f 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -405,6 +405,25 @@ public final class BluetoothAdapter {
}
/**
+ * Get a {@link BluetoothDevice} object for the given Bluetooth hardware
+ * address.
+ * <p>Valid Bluetooth hardware addresses must be 6 bytes. This method
+ * expects the address in network byte order (MSB first).
+ * <p>A {@link BluetoothDevice} will always be returned for a valid
+ * hardware address, even if this adapter has never seen that device.
+ *
+ * @param address Bluetooth MAC address (6 bytes)
+ * @throws IllegalArgumentException if address is invalid
+ */
+ public BluetoothDevice getRemoteDevice(byte[] address) {
+ if (address == null || address.length != 6) {
+ throw new IllegalArgumentException("Bluetooth address must have 6 bytes");
+ }
+ return new BluetoothDevice(String.format("%02X:%02X:%02X:%02X:%02X:%02X",
+ address[0], address[1], address[2], address[3], address[4], address[5]));
+ }
+
+ /**
* Return true if Bluetooth is currently enabled and ready for use.
* <p>Equivalent to:
* <code>getBluetoothState() == STATE_ON</code>
@@ -1287,7 +1306,7 @@ public final class BluetoothAdapter {
}
/**
- * Validate a Bluetooth address, such as "00:43:A8:23:10:F0"
+ * Validate a String Bluetooth address, such as "00:43:A8:23:10:F0"
* <p>Alphabetic characters must be uppercase to be valid.
*
* @param address Bluetooth address as string
diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java
index 0b54396..944ca6b 100644
--- a/core/java/android/content/AsyncTaskLoader.java
+++ b/core/java/android/content/AsyncTaskLoader.java
@@ -173,6 +173,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> {
if (DEBUG) Slog.v(TAG, "cancelLoad: cancelled=" + cancelled);
if (cancelled) {
mCancellingTask = mTask;
+ onCancelLoadInBackground();
}
mTask = null;
return cancelled;
@@ -256,6 +257,25 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> {
}
/**
+ * Override this method to try to abort the computation currently taking
+ * place on a background thread.
+ *
+ * Note that when this method is called, it is possible that {@link #loadInBackground}
+ * has not started yet or has already completed.
+ */
+ protected void onCancelLoadInBackground() {
+ }
+
+ /**
+ * Returns true if the current execution of {@link #loadInBackground()} is being canceled.
+ *
+ * @return True if the current execution of {@link #loadInBackground()} is being canceled.
+ */
+ protected boolean isLoadInBackgroundCanceled() {
+ return mCancellingTask != null;
+ }
+
+ /**
* Locks the current thread until the loader completes the current load
* operation. Returns immediately if there is no load operation running.
* Should not be called from the UI thread: calling it from the UI
diff --git a/core/java/android/content/CancelationSignal.java b/core/java/android/content/CancelationSignal.java
new file mode 100644
index 0000000..58cf59d
--- /dev/null
+++ b/core/java/android/content/CancelationSignal.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.RemoteException;
+
+/**
+ * Provides the ability to cancel an operation in progress.
+ */
+public final class CancelationSignal {
+ private boolean mIsCanceled;
+ private OnCancelListener mOnCancelListener;
+ private ICancelationSignal mRemote;
+
+ /**
+ * Creates a cancelation signal, initially not canceled.
+ */
+ public CancelationSignal() {
+ }
+
+ /**
+ * Returns true if the operation has been canceled.
+ *
+ * @return True if the operation has been canceled.
+ */
+ public boolean isCanceled() {
+ synchronized (this) {
+ return mIsCanceled;
+ }
+ }
+
+ /**
+ * Throws {@link OperationCanceledException} if the operation has been canceled.
+ *
+ * @throws OperationCanceledException if the operation has been canceled.
+ */
+ public void throwIfCanceled() {
+ if (isCanceled()) {
+ throw new OperationCanceledException();
+ }
+ }
+
+ /**
+ * Cancels the operation and signals the cancelation listener.
+ * If the operation has not yet started, then it will be canceled as soon as it does.
+ */
+ public void cancel() {
+ synchronized (this) {
+ if (!mIsCanceled) {
+ mIsCanceled = true;
+ if (mOnCancelListener != null) {
+ mOnCancelListener.onCancel();
+ }
+ if (mRemote != null) {
+ try {
+ mRemote.cancel();
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the cancelation listener to be called when canceled.
+ * If {@link CancelationSignal#cancel} has already been called, then the provided
+ * listener is invoked immediately.
+ *
+ * The listener is called while holding the cancelation signal's lock which is
+ * also held while registering or unregistering the listener. Because of the lock,
+ * it is not possible for the listener to run after it has been unregistered.
+ * This design choice makes it easier for clients of {@link CancelationSignal} to
+ * prevent race conditions related to listener registration and unregistration.
+ *
+ * @param listener The cancelation listener, or null to remove the current listener.
+ */
+ public void setOnCancelListener(OnCancelListener listener) {
+ synchronized (this) {
+ mOnCancelListener = listener;
+ if (mIsCanceled && listener != null) {
+ listener.onCancel();
+ }
+ }
+ }
+
+ /**
+ * Sets the remote transport.
+ *
+ * @param remote The remote transport, or null to remove.
+ *
+ * @hide
+ */
+ public void setRemote(ICancelationSignal remote) {
+ synchronized (this) {
+ mRemote = remote;
+ if (mIsCanceled && remote != null) {
+ try {
+ remote.cancel();
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a transport that can be returned back to the caller of
+ * a Binder function and subsequently used to dispatch a cancelation signal.
+ *
+ * @return The new cancelation signal transport.
+ *
+ * @hide
+ */
+ public static ICancelationSignal createTransport() {
+ return new Transport();
+ }
+
+ /**
+ * Given a locally created transport, returns its associated cancelation signal.
+ *
+ * @param transport The locally created transport, or null if none.
+ * @return The associated cancelation signal, or null if none.
+ *
+ * @hide
+ */
+ public static CancelationSignal fromTransport(ICancelationSignal transport) {
+ if (transport instanceof Transport) {
+ return ((Transport)transport).mCancelationSignal;
+ }
+ return null;
+ }
+
+ /**
+ * Listens for cancelation.
+ */
+ public interface OnCancelListener {
+ /**
+ * Called when {@link CancelationSignal#cancel} is invoked.
+ */
+ void onCancel();
+ }
+
+ private static final class Transport extends ICancelationSignal.Stub {
+ final CancelationSignal mCancelationSignal = new CancelationSignal();
+
+ @Override
+ public void cancel() throws RemoteException {
+ mCancelationSignal.cancel();
+ }
+ }
+}
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index 092a0c8..adbeb6a 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -29,11 +29,14 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.RemoteException;
import android.util.Log;
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
/**
@@ -172,28 +175,33 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
return getContentProvider().getClass().getName();
}
+ @Override
public Cursor query(Uri uri, String[] projection,
- String selection, String[] selectionArgs, String sortOrder) {
+ String selection, String[] selectionArgs, String sortOrder,
+ ICancelationSignal cancelationSignal) {
enforceReadPermission(uri);
- return ContentProvider.this.query(uri, projection, selection,
- selectionArgs, sortOrder);
+ return ContentProvider.this.query(uri, projection, selection, selectionArgs, sortOrder,
+ CancelationSignal.fromTransport(cancelationSignal));
}
+ @Override
public String getType(Uri uri) {
return ContentProvider.this.getType(uri);
}
-
+ @Override
public Uri insert(Uri uri, ContentValues initialValues) {
enforceWritePermission(uri);
return ContentProvider.this.insert(uri, initialValues);
}
+ @Override
public int bulkInsert(Uri uri, ContentValues[] initialValues) {
enforceWritePermission(uri);
return ContentProvider.this.bulkInsert(uri, initialValues);
}
+ @Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
for (ContentProviderOperation operation : operations) {
@@ -208,17 +216,20 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
return ContentProvider.this.applyBatch(operations);
}
+ @Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
enforceWritePermission(uri);
return ContentProvider.this.delete(uri, selection, selectionArgs);
}
+ @Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
enforceWritePermission(uri);
return ContentProvider.this.update(uri, values, selection, selectionArgs);
}
+ @Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri);
@@ -226,6 +237,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
return ContentProvider.this.openFile(uri, mode);
}
+ @Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode)
throws FileNotFoundException {
if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri);
@@ -233,6 +245,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
return ContentProvider.this.openAssetFile(uri, mode);
}
+ @Override
public Bundle call(String method, String arg, Bundle extras) {
return ContentProvider.this.call(method, arg, extras);
}
@@ -249,6 +262,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
return ContentProvider.this.openTypedAssetFile(uri, mimeType, opts);
}
+ @Override
+ public ICancelationSignal createCancelationSignal() throws RemoteException {
+ return CancelationSignal.createTransport();
+ }
+
private void enforceReadPermission(Uri uri) {
final int uid = Binder.getCallingUid();
if (uid == mMyUid) {
@@ -539,6 +557,75 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
String selection, String[] selectionArgs, String sortOrder);
/**
+ * Implement this to handle query requests from clients with support for cancelation.
+ * This method can be called from multiple threads, as described in
+ * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
+ * and Threads</a>.
+ * <p>
+ * Example client call:<p>
+ * <pre>// Request a specific record.
+ * Cursor managedCursor = managedQuery(
+ ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2),
+ projection, // Which columns to return.
+ null, // WHERE clause.
+ null, // WHERE clause value substitution
+ People.NAME + " ASC"); // Sort order.</pre>
+ * Example implementation:<p>
+ * <pre>// SQLiteQueryBuilder is a helper class that creates the
+ // proper SQL syntax for us.
+ SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder();
+
+ // Set the table we're querying.
+ qBuilder.setTables(DATABASE_TABLE_NAME);
+
+ // If the query ends in a specific record number, we're
+ // being asked for a specific record, so set the
+ // WHERE clause in our query.
+ if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){
+ qBuilder.appendWhere("_id=" + uri.getPathLeafId());
+ }
+
+ // Make the query.
+ Cursor c = qBuilder.query(mDb,
+ projection,
+ selection,
+ selectionArgs,
+ groupBy,
+ having,
+ sortOrder);
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;</pre>
+ * <p>
+ * If you implement this method then you must also implement the version of
+ * {@link #query(Uri, String[], String, String[], String)} that does not take a cancelation
+ * provider to ensure correct operation on older versions of the Android Framework in
+ * which the cancelation signal overload was not available.
+ *
+ * @param uri The URI to query. This will be the full URI sent by the client;
+ * if the client is requesting a specific record, the URI will end in a record number
+ * that the implementation should parse and add to a WHERE or HAVING clause, specifying
+ * that _id value.
+ * @param projection The list of columns to put into the cursor. If
+ * null all columns are included.
+ * @param selection A selection criteria to apply when filtering rows.
+ * If null then all rows are included.
+ * @param selectionArgs You may include ?s in selection, which will be replaced by
+ * the values from selectionArgs, in order that they appear in the selection.
+ * The values will be bound as Strings.
+ * @param sortOrder How the rows in the cursor should be sorted.
+ * If null then the provider is free to define the sort order.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return a Cursor or null.
+ */
+ public Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder,
+ CancelationSignal cancelationSignal) {
+ return query(uri, projection, selection, selectionArgs, sortOrder);
+ }
+
+ /**
* Implement this to handle requests for the MIME type of the data at the
* given URI. The returned MIME type should start with
* <code>vnd.android.cursor.item</code> for a single record,
@@ -1013,4 +1100,19 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
Log.w(TAG, "implement ContentProvider shutdown() to make sure all database " +
"connections are gracefully shutdown");
}
+
+ /**
+ * Print the Provider's state into the given stream. This gets invoked if
+ * you run "adb shell dumpsys activity provider <provider_component_name>".
+ *
+ * @param prefix Desired prefix to prepend at each line of output.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ * @hide
+ */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.println("nothing to dump");
+ }
}
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 0540109..9a1fa65 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -47,7 +47,20 @@ public class ContentProviderClient {
/** See {@link ContentProvider#query ContentProvider.query} */
public Cursor query(Uri url, String[] projection, String selection,
String[] selectionArgs, String sortOrder) throws RemoteException {
- return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder);
+ return query(url, projection, selection, selectionArgs, sortOrder, null);
+ }
+
+ /** See {@link ContentProvider#query ContentProvider.query} */
+ public Cursor query(Uri url, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder, CancelationSignal cancelationSignal)
+ throws RemoteException {
+ ICancelationSignal remoteCancelationSignal = null;
+ if (cancelationSignal != null) {
+ remoteCancelationSignal = mContentProvider.createCancelationSignal();
+ cancelationSignal.setRemote(remoteCancelationSignal);
+ }
+ return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder,
+ remoteCancelationSignal);
}
/** See {@link ContentProvider#getType ContentProvider.getType} */
diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java
index b089bf2..e0e277a 100644
--- a/core/java/android/content/ContentProviderNative.java
+++ b/core/java/android/content/ContentProviderNative.java
@@ -21,7 +21,6 @@ import android.database.BulkCursorNative;
import android.database.BulkCursorToCursorAdaptor;
import android.database.Cursor;
import android.database.CursorToBulkCursorAdaptor;
-import android.database.CursorWindow;
import android.database.DatabaseUtils;
import android.database.IBulkCursor;
import android.database.IContentObserver;
@@ -41,8 +40,6 @@ import java.util.ArrayList;
* {@hide}
*/
abstract public class ContentProviderNative extends Binder implements IContentProvider {
- private static final String TAG = "ContentProvider";
-
public ContentProviderNative()
{
attachInterface(this, descriptor);
@@ -108,8 +105,11 @@ abstract public class ContentProviderNative extends Binder implements IContentPr
String sortOrder = data.readString();
IContentObserver observer = IContentObserver.Stub.asInterface(
data.readStrongBinder());
+ ICancelationSignal cancelationSignal = ICancelationSignal.Stub.asInterface(
+ data.readStrongBinder());
- Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder);
+ Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder,
+ cancelationSignal);
if (cursor != null) {
CursorToBulkCursorAdaptor adaptor = new CursorToBulkCursorAdaptor(
cursor, observer, getProviderName());
@@ -295,6 +295,16 @@ abstract public class ContentProviderNative extends Binder implements IContentPr
}
return true;
}
+
+ case CREATE_CANCELATION_SIGNAL_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+
+ ICancelationSignal cancelationSignal = createCancelationSignal();
+ reply.writeNoException();
+ reply.writeStrongBinder(cancelationSignal.asBinder());
+ return true;
+ }
}
} catch (Exception e) {
DatabaseUtils.writeExceptionToParcel(reply, e);
@@ -324,7 +334,8 @@ final class ContentProviderProxy implements IContentProvider
}
public Cursor query(Uri url, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) throws RemoteException {
+ String[] selectionArgs, String sortOrder, ICancelationSignal cancelationSignal)
+ throws RemoteException {
BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor();
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
@@ -352,6 +363,7 @@ final class ContentProviderProxy implements IContentProvider
}
data.writeString(sortOrder);
data.writeStrongBinder(adaptor.getObserver().asBinder());
+ data.writeStrongBinder(cancelationSignal != null ? cancelationSignal.asBinder() : null);
mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);
@@ -620,5 +632,24 @@ final class ContentProviderProxy implements IContentProvider
}
}
+ public ICancelationSignal createCancelationSignal() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ mRemote.transact(IContentProvider.CREATE_CANCELATION_SIGNAL_TRANSACTION,
+ data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ ICancelationSignal cancelationSignal = ICancelationSignal.Stub.asInterface(
+ reply.readStrongBinder());
+ return cancelationSignal;
+ } finally {
+ data.recycle();
+ reply.recycle();
+ }
+ }
+
private IBinder mRemote;
}
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index cc3219b..e79475a 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -22,6 +22,7 @@ import android.accounts.Account;
import android.app.ActivityManagerNative;
import android.app.ActivityThread;
import android.app.AppGlobals;
+import android.content.ContentProvider.Transport;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
@@ -302,13 +303,62 @@ public abstract class ContentResolver {
*/
public final Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
+ return query(uri, projection, selection, selectionArgs, sortOrder, null);
+ }
+
+ /**
+ * <p>
+ * Query the given URI, returning a {@link Cursor} over the result set.
+ * </p>
+ * <p>
+ * For best performance, the caller should follow these guidelines:
+ * <ul>
+ * <li>Provide an explicit projection, to prevent
+ * reading data from storage that aren't going to be used.</li>
+ * <li>Use question mark parameter markers such as 'phone=?' instead of
+ * explicit values in the {@code selection} parameter, so that queries
+ * that differ only by those values will be recognized as the same
+ * for caching purposes.</li>
+ * </ul>
+ * </p>
+ *
+ * @param uri The URI, using the content:// scheme, for the content to
+ * retrieve.
+ * @param projection A list of which columns to return. Passing null will
+ * return all columns, which is inefficient.
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null will
+ * return all rows for the given URI.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in the order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @param sortOrder How to order the rows, formatted as an SQL ORDER BY
+ * clause (excluding the ORDER BY itself). Passing null will use the
+ * default sort order, which may be unordered.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A Cursor object, which is positioned before the first entry, or null
+ * @see Cursor
+ */
+ public final Cursor query(final Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder,
+ CancelationSignal cancelationSignal) {
IContentProvider provider = acquireProvider(uri);
if (provider == null) {
return null;
}
try {
long startTime = SystemClock.uptimeMillis();
- Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder);
+
+ ICancelationSignal remoteCancelationSignal = null;
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ remoteCancelationSignal = provider.createCancelationSignal();
+ cancelationSignal.setRemote(remoteCancelationSignal);
+ }
+ Cursor qCursor = provider.query(uri, projection,
+ selection, selectionArgs, sortOrder, remoteCancelationSignal);
if (qCursor == null) {
releaseProvider(provider);
return null;
@@ -1034,8 +1084,11 @@ public abstract class ContentResolver {
* To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}.
* By default, CursorAdapter objects will get this notification.
*
- * @param uri
- * @param observer The observer that originated the change, may be <code>null</null>
+ * @param uri The uri of the content that was changed.
+ * @param observer The observer that originated the change, may be <code>null</null>.
+ * The observer that originated the change will only receive the notification if it
+ * has requested to receive self-change notifications by implementing
+ * {@link ContentObserver#deliverSelfNotifications()} to return true.
*/
public void notifyChange(Uri uri, ContentObserver observer) {
notifyChange(uri, observer, true /* sync to network */);
@@ -1046,8 +1099,11 @@ public abstract class ContentResolver {
* To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}.
* By default, CursorAdapter objects will get this notification.
*
- * @param uri
- * @param observer The observer that originated the change, may be <code>null</null>
+ * @param uri The uri of the content that was changed.
+ * @param observer The observer that originated the change, may be <code>null</null>.
+ * The observer that originated the change will only receive the notification if it
+ * has requested to receive self-change notifications by implementing
+ * {@link ContentObserver#deliverSelfNotifications()} to return true.
* @param syncToNetwork If true, attempt to sync the change to the network.
*/
public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java
index 0e83dc0..fc4c262 100644
--- a/core/java/android/content/ContentService.java
+++ b/core/java/android/content/ContentService.java
@@ -176,7 +176,7 @@ public final class ContentService extends IContentService.Stub {
for (int i=0; i<numCalls; i++) {
ObserverCall oc = calls.get(i);
try {
- oc.mObserver.onChange(oc.mSelfNotify);
+ oc.mObserver.onChange(oc.mSelfChange, uri);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri);
}
@@ -218,13 +218,12 @@ public final class ContentService extends IContentService.Stub {
public static final class ObserverCall {
final ObserverNode mNode;
final IContentObserver mObserver;
- final boolean mSelfNotify;
+ final boolean mSelfChange;
- ObserverCall(ObserverNode node, IContentObserver observer,
- boolean selfNotify) {
+ ObserverCall(ObserverNode node, IContentObserver observer, boolean selfChange) {
mNode = node;
mObserver = observer;
- mSelfNotify = selfNotify;
+ mSelfChange = selfChange;
}
}
@@ -668,7 +667,7 @@ public final class ContentService extends IContentService.Stub {
}
private void collectMyObserversLocked(boolean leaf, IContentObserver observer,
- boolean selfNotify, ArrayList<ObserverCall> calls) {
+ boolean observerWantsSelfNotifications, ArrayList<ObserverCall> calls) {
int N = mObservers.size();
IBinder observerBinder = observer == null ? null : observer.asBinder();
for (int i = 0; i < N; i++) {
@@ -676,28 +675,29 @@ public final class ContentService extends IContentService.Stub {
// Don't notify the observer if it sent the notification and isn't interesed
// in self notifications
- if (entry.observer.asBinder() == observerBinder && !selfNotify) {
+ boolean selfChange = (entry.observer.asBinder() == observerBinder);
+ if (selfChange && !observerWantsSelfNotifications) {
continue;
}
// Make sure the observer is interested in the notification
if (leaf || (!leaf && entry.notifyForDescendents)) {
- calls.add(new ObserverCall(this, entry.observer, selfNotify));
+ calls.add(new ObserverCall(this, entry.observer, selfChange));
}
}
}
public void collectObserversLocked(Uri uri, int index, IContentObserver observer,
- boolean selfNotify, ArrayList<ObserverCall> calls) {
+ boolean observerWantsSelfNotifications, ArrayList<ObserverCall> calls) {
String segment = null;
int segmentCount = countUriSegments(uri);
if (index >= segmentCount) {
// This is the leaf node, notify all observers
- collectMyObserversLocked(true, observer, selfNotify, calls);
+ collectMyObserversLocked(true, observer, observerWantsSelfNotifications, calls);
} else if (index < segmentCount){
segment = getUriSegment(uri, index);
// Notify any observers at this level who are interested in descendents
- collectMyObserversLocked(false, observer, selfNotify, calls);
+ collectMyObserversLocked(false, observer, observerWantsSelfNotifications, calls);
}
int N = mChildren.size();
@@ -705,7 +705,8 @@ public final class ContentService extends IContentService.Stub {
ObserverNode node = mChildren.get(i);
if (segment == null || node.mName.equals(segment)) {
// We found the child,
- node.collectObserversLocked(uri, index + 1, observer, selfNotify, calls);
+ node.collectObserversLocked(uri, index + 1,
+ observer, observerWantsSelfNotifications, calls);
if (segment != null) {
break;
}
diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java
index 7af535b..6e4aca8 100644
--- a/core/java/android/content/CursorLoader.java
+++ b/core/java/android/content/CursorLoader.java
@@ -19,7 +19,6 @@ package android.content;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
-import android.os.AsyncTask;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -49,18 +48,42 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> {
String mSortOrder;
Cursor mCursor;
+ CancelationSignal mCancelationSignal;
/* Runs on a worker thread */
@Override
public Cursor loadInBackground() {
- Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
- mSelectionArgs, mSortOrder);
- if (cursor != null) {
- // Ensure the cursor window is filled
- cursor.getCount();
- registerContentObserver(cursor, mObserver);
+ synchronized (this) {
+ if (isLoadInBackgroundCanceled()) {
+ throw new OperationCanceledException();
+ }
+ mCancelationSignal = new CancelationSignal();
+ }
+ try {
+ Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
+ mSelectionArgs, mSortOrder, mCancelationSignal);
+ if (cursor != null) {
+ // Ensure the cursor window is filled
+ cursor.getCount();
+ registerContentObserver(cursor, mObserver);
+ }
+ return cursor;
+ } finally {
+ synchronized (this) {
+ mCancelationSignal = null;
+ }
+ }
+ }
+
+ @Override
+ protected void onCancelLoadInBackground() {
+ super.onCancelLoadInBackground();
+
+ synchronized (this) {
+ if (mCancelationSignal != null) {
+ mCancelationSignal.cancel();
+ }
}
- return cursor;
}
/**
diff --git a/core/java/android/nfc/LlcpPacket.aidl b/core/java/android/content/ICancelationSignal.aidl
index 80f424d..3f5a24d 100644
--- a/core/java/android/nfc/LlcpPacket.aidl
+++ b/core/java/android/content/ICancelationSignal.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2012 The Android Open 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,9 +14,11 @@
* limitations under the License.
*/
-package android.nfc;
+package android.content;
/**
* @hide
*/
-parcelable LlcpPacket; \ No newline at end of file
+interface ICancelationSignal {
+ oneway void cancel();
+}
diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java
index 2a67ff8..f52157f 100644
--- a/core/java/android/content/IContentProvider.java
+++ b/core/java/android/content/IContentProvider.java
@@ -34,7 +34,8 @@ import java.util.ArrayList;
*/
public interface IContentProvider extends IInterface {
public Cursor query(Uri url, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) throws RemoteException;
+ String[] selectionArgs, String sortOrder, ICancelationSignal cancelationSignal)
+ throws RemoteException;
public String getType(Uri url) throws RemoteException;
public Uri insert(Uri url, ContentValues initialValues)
throws RemoteException;
@@ -50,6 +51,7 @@ public interface IContentProvider extends IInterface {
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws RemoteException, OperationApplicationException;
public Bundle call(String method, String arg, Bundle extras) throws RemoteException;
+ public ICancelationSignal createCancelationSignal() throws RemoteException;
// Data interchange.
public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException;
@@ -71,4 +73,5 @@ public interface IContentProvider extends IInterface {
static final int CALL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 20;
static final int GET_STREAM_TYPES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 21;
static final int OPEN_TYPED_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 22;
+ static final int CREATE_CANCELATION_SIGNAL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 23;
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index e3b1f54..fbc1b2b 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -43,6 +43,7 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.Locale;
import java.util.Set;
/**
@@ -4420,22 +4421,24 @@ public class Intent implements Parcelable, Cloneable {
/**
* Set the data this intent is operating on. This method automatically
- * clears any type that was previously set by {@link #setType}.
+ * clears any type that was previously set by {@link #setType} or
+ * {@link #setTypeAndNormalize}.
*
- * <p><em>Note: scheme and host name matching in the Android framework is
- * case-sensitive, unlike the formal RFC. As a result,
- * you should always ensure that you write your Uri with these elements
- * using lower case letters, and normalize any Uris you receive from
- * outside of Android to ensure the scheme and host is lower case.</em></p>
+ * <p><em>Note: scheme matching in the Android framework is
+ * case-sensitive, unlike the formal RFC. As a result,
+ * you should always write your Uri with a lower case scheme,
+ * or use {@link Uri#normalize} or
+ * {@link #setDataAndNormalize}
+ * to ensure that the scheme is converted to lower case.</em>
*
- * @param data The URI of the data this intent is now targeting.
+ * @param data The Uri of the data this intent is now targeting.
*
* @return Returns the same Intent object, for chaining multiple calls
* into a single statement.
*
* @see #getData
- * @see #setType
- * @see #setDataAndType
+ * @see #setDataAndNormalize
+ * @see android.net.Intent#normalize
*/
public Intent setData(Uri data) {
mData = data;
@@ -4444,16 +4447,45 @@ public class Intent implements Parcelable, Cloneable {
}
/**
- * Set an explicit MIME data type. This is used to create intents that
- * only specify a type and not data, for example to indicate the type of
- * data to return. This method automatically clears any data that was
- * previously set by {@link #setData}.
+ * Normalize and set the data this intent is operating on.
+ *
+ * <p>This method automatically clears any type that was
+ * previously set (for example, by {@link #setType}).
+ *
+ * <p>The data Uri is normalized using
+ * {@link android.net.Uri#normalize} before it is set,
+ * so really this is just a convenience method for
+ * <pre>
+ * setData(data.normalize())
+ * </pre>
+ *
+ * @param data The Uri of the data this intent is now targeting.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #getData
+ * @see #setType
+ * @see android.net.Uri#normalize
+ */
+ public Intent setDataAndNormalize(Uri data) {
+ return setData(data.normalize());
+ }
+
+ /**
+ * Set an explicit MIME data type.
+ *
+ * <p>This is used to create intents that only specify a type and not data,
+ * for example to indicate the type of data to return.
+ *
+ * <p>This method automatically clears any data that was
+ * previously set (for example by {@link #setData}).
*
* <p><em>Note: MIME type matching in the Android framework is
* case-sensitive, unlike formal RFC MIME types. As a result,
* you should always write your MIME types with lower case letters,
- * and any MIME types you receive from outside of Android should be
- * converted to lower case before supplying them here.</em></p>
+ * or use {@link #normalizeMimeType} or {@link #setTypeAndNormalize}
+ * to ensure that it is converted to lower case.</em>
*
* @param type The MIME type of the data being handled by this intent.
*
@@ -4461,8 +4493,9 @@ public class Intent implements Parcelable, Cloneable {
* into a single statement.
*
* @see #getType
- * @see #setData
+ * @see #setTypeAndNormalize
* @see #setDataAndType
+ * @see #normalizeMimeType
*/
public Intent setType(String type) {
mData = null;
@@ -4471,26 +4504,58 @@ public class Intent implements Parcelable, Cloneable {
}
/**
+ * Normalize and set an explicit MIME data type.
+ *
+ * <p>This is used to create intents that only specify a type and not data,
+ * for example to indicate the type of data to return.
+ *
+ * <p>This method automatically clears any data that was
+ * previously set (for example by {@link #setData}).
+ *
+ * <p>The MIME type is normalized using
+ * {@link #normalizeMimeType} before it is set,
+ * so really this is just a convenience method for
+ * <pre>
+ * setType(Intent.normalizeMimeType(type))
+ * </pre>
+ *
+ * @param type The MIME type of the data being handled by this intent.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #getType
+ * @see #setData
+ * @see #normalizeMimeType
+ */
+ public Intent setTypeAndNormalize(String type) {
+ return setType(normalizeMimeType(type));
+ }
+
+ /**
* (Usually optional) Set the data for the intent along with an explicit
* MIME data type. This method should very rarely be used -- it allows you
* to override the MIME type that would ordinarily be inferred from the
* data with your own type given here.
*
- * <p><em>Note: MIME type, Uri scheme, and host name matching in the
+ * <p><em>Note: MIME type and Uri scheme matching in the
* Android framework is case-sensitive, unlike the formal RFC definitions.
* As a result, you should always write these elements with lower case letters,
- * and normalize any MIME types or Uris you receive from
- * outside of Android to ensure these elements are lower case before
- * supplying them here.</em></p>
+ * or use {@link #normalizeMimeType} or {@link android.net.Uri#normalize} or
+ * {@link #setDataAndTypeAndNormalize}
+ * to ensure that they are converted to lower case.</em>
*
- * @param data The URI of the data this intent is now targeting.
+ * @param data The Uri of the data this intent is now targeting.
* @param type The MIME type of the data being handled by this intent.
*
* @return Returns the same Intent object, for chaining multiple calls
* into a single statement.
*
- * @see #setData
* @see #setType
+ * @see #setData
+ * @see #normalizeMimeType
+ * @see android.net.Uri#normalize
+ * @see #setDataAndTypeAndNormalize
*/
public Intent setDataAndType(Uri data, String type) {
mData = data;
@@ -4499,6 +4564,35 @@ public class Intent implements Parcelable, Cloneable {
}
/**
+ * (Usually optional) Normalize and set both the data Uri and an explicit
+ * MIME data type. This method should very rarely be used -- it allows you
+ * to override the MIME type that would ordinarily be inferred from the
+ * data with your own type given here.
+ *
+ * <p>The data Uri and the MIME type are normalize using
+ * {@link android.net.Uri#normalize} and {@link #normalizeMimeType}
+ * before they are set, so really this is just a convenience method for
+ * <pre>
+ * setDataAndType(data.normalize(), Intent.normalizeMimeType(type))
+ * </pre>
+ *
+ * @param data The Uri of the data this intent is now targeting.
+ * @param type The MIME type of the data being handled by this intent.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #setType
+ * @see #setData
+ * @see #setDataAndType
+ * @see #normalizeMimeType
+ * @see android.net.Uri#normalize
+ */
+ public Intent setDataAndTypeAndNormalize(Uri data, String type) {
+ return setDataAndType(data.normalize(), normalizeMimeType(type));
+ }
+
+ /**
* Add a new category to the intent. Categories provide additional detail
* about the action the intent is perform. When resolving an intent, only
* activities that provide <em>all</em> of the requested categories will be
@@ -5566,7 +5660,7 @@ public class Intent implements Parcelable, Cloneable {
*
* <ul>
* <li> action, as set by {@link #setAction}.
- * <li> data URI and MIME type, as set by {@link #setData(Uri)},
+ * <li> data Uri and MIME type, as set by {@link #setData(Uri)},
* {@link #setType(String)}, or {@link #setDataAndType(Uri, String)}.
* <li> categories, as set by {@link #addCategory}.
* <li> package, as set by {@link #setPackage}.
@@ -6229,4 +6323,38 @@ public class Intent implements Parcelable, Cloneable {
return intent;
}
+
+ /**
+ * Normalize a MIME data type.
+ *
+ * <p>A normalized MIME type has white-space trimmed,
+ * content-type parameters removed, and is lower-case.
+ * This aligns the type with Android best practices for
+ * intent filtering.
+ *
+ * <p>For example, "text/plain; charset=utf-8" becomes "text/plain".
+ * "text/x-vCard" becomes "text/x-vcard".
+ *
+ * <p>All MIME types received from outside Android (such as user input,
+ * or external sources like Bluetooth, NFC, or the Internet) should
+ * be normalized before they are used to create an Intent.
+ *
+ * @param type MIME data type to normalize
+ * @return normalized MIME data type, or null if the input was null
+ * @see {@link #setType}
+ * @see {@link #setTypeAndNormalize}
+ */
+ public static String normalizeMimeType(String type) {
+ if (type == null) {
+ return null;
+ }
+
+ type = type.trim().toLowerCase(Locale.US);
+
+ final int semicolonIndex = type.indexOf(';');
+ if (semicolonIndex != -1) {
+ type = type.substring(0, semicolonIndex);
+ }
+ return type;
+ }
}
diff --git a/core/java/android/content/OperationCanceledException.java b/core/java/android/content/OperationCanceledException.java
new file mode 100644
index 0000000..24afcfa
--- /dev/null
+++ b/core/java/android/content/OperationCanceledException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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;
+
+/**
+ * An exception type that is thrown when an operation in progress is canceled.
+ *
+ * @see CancelationSignal
+ */
+public class OperationCanceledException extends RuntimeException {
+ public OperationCanceledException() {
+ this(null);
+ }
+
+ public OperationCanceledException(String message) {
+ super(message != null ? message : "The operation has been canceled.");
+ }
+}
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
index 3c4e545..ba24036 100644
--- a/core/java/android/content/SyncManager.java
+++ b/core/java/android/content/SyncManager.java
@@ -1034,6 +1034,7 @@ public class SyncManager implements OnAccountsUpdateListener {
protected void dumpSyncState(PrintWriter pw) {
pw.print("data connected: "); pw.println(mDataConnectionIsConnected);
+ pw.print("auto sync: "); pw.println(mSyncStorageEngine.getMasterSyncAutomatically());
pw.print("memory low: "); pw.println(mStorageIsLow);
final Account[] accounts = mAccounts;
@@ -1272,57 +1273,17 @@ public class SyncManager implements OnAccountsUpdateListener {
}
- pw.println();
- pw.printf("Detailed Statistics (Recent history): %d (# of times) %ds (sync time)\n",
- totalTimes, totalElapsedTime / 1000);
-
- final List<AuthoritySyncStats> sortedAuthorities =
- new ArrayList<AuthoritySyncStats>(authorityMap.values());
- Collections.sort(sortedAuthorities, new Comparator<AuthoritySyncStats>() {
- @Override
- public int compare(AuthoritySyncStats lhs, AuthoritySyncStats rhs) {
- // reverse order
- int compare = Integer.compare(rhs.times, lhs.times);
- if (compare == 0) {
- compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime);
- }
- return compare;
- }
- });
-
- final int maxLength = Math.max(maxAuthority, maxAccount + 3);
- final int padLength = 2 + 2 + maxLength + 2 + 10 + 11;
- final char chars[] = new char[padLength];
- Arrays.fill(chars, '-');
- final String separator = new String(chars);
-
- final String authorityFormat = String.format(" %%-%ds: %%-9s %%-11s\n", maxLength + 2);
- final String accountFormat = String.format(" %%-%ds: %%-9s %%-11s\n", maxLength);
-
- pw.println(separator);
- for (AuthoritySyncStats authoritySyncStats : sortedAuthorities) {
- String name = authoritySyncStats.name;
- long elapsedTime;
- int times;
- String timeStr;
- String timesStr;
-
- elapsedTime = authoritySyncStats.elapsedTime;
- times = authoritySyncStats.times;
- timeStr = String.format("%ds/%d%%",
- elapsedTime / 1000,
- elapsedTime * 100 / totalElapsedTime);
- timesStr = String.format("%d/%d%%",
- times,
- times * 100 / totalTimes);
- pw.printf(authorityFormat, name, timesStr, timeStr);
-
- final List<AccountSyncStats> sortedAccounts =
- new ArrayList<AccountSyncStats>(
- authoritySyncStats.accountMap.values());
- Collections.sort(sortedAccounts, new Comparator<AccountSyncStats>() {
+ if (totalElapsedTime > 0) {
+ pw.println();
+ pw.printf("Detailed Statistics (Recent history): "
+ + "%d (# of times) %ds (sync time)\n",
+ totalTimes, totalElapsedTime / 1000);
+
+ final List<AuthoritySyncStats> sortedAuthorities =
+ new ArrayList<AuthoritySyncStats>(authorityMap.values());
+ Collections.sort(sortedAuthorities, new Comparator<AuthoritySyncStats>() {
@Override
- public int compare(AccountSyncStats lhs, AccountSyncStats rhs) {
+ public int compare(AuthoritySyncStats lhs, AuthoritySyncStats rhs) {
// reverse order
int compare = Integer.compare(rhs.times, lhs.times);
if (compare == 0) {
@@ -1331,18 +1292,63 @@ public class SyncManager implements OnAccountsUpdateListener {
return compare;
}
});
- for (AccountSyncStats stats: sortedAccounts) {
- elapsedTime = stats.elapsedTime;
- times = stats.times;
+
+ final int maxLength = Math.max(maxAuthority, maxAccount + 3);
+ final int padLength = 2 + 2 + maxLength + 2 + 10 + 11;
+ final char chars[] = new char[padLength];
+ Arrays.fill(chars, '-');
+ final String separator = new String(chars);
+
+ final String authorityFormat =
+ String.format(" %%-%ds: %%-9s %%-11s\n", maxLength + 2);
+ final String accountFormat =
+ String.format(" %%-%ds: %%-9s %%-11s\n", maxLength);
+
+ pw.println(separator);
+ for (AuthoritySyncStats authoritySyncStats : sortedAuthorities) {
+ String name = authoritySyncStats.name;
+ long elapsedTime;
+ int times;
+ String timeStr;
+ String timesStr;
+
+ elapsedTime = authoritySyncStats.elapsedTime;
+ times = authoritySyncStats.times;
timeStr = String.format("%ds/%d%%",
elapsedTime / 1000,
elapsedTime * 100 / totalElapsedTime);
timesStr = String.format("%d/%d%%",
times,
times * 100 / totalTimes);
- pw.printf(accountFormat, stats.name, timesStr, timeStr);
+ pw.printf(authorityFormat, name, timesStr, timeStr);
+
+ final List<AccountSyncStats> sortedAccounts =
+ new ArrayList<AccountSyncStats>(
+ authoritySyncStats.accountMap.values());
+ Collections.sort(sortedAccounts, new Comparator<AccountSyncStats>() {
+ @Override
+ public int compare(AccountSyncStats lhs, AccountSyncStats rhs) {
+ // reverse order
+ int compare = Integer.compare(rhs.times, lhs.times);
+ if (compare == 0) {
+ compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime);
+ }
+ return compare;
+ }
+ });
+ for (AccountSyncStats stats: sortedAccounts) {
+ elapsedTime = stats.elapsedTime;
+ times = stats.times;
+ timeStr = String.format("%ds/%d%%",
+ elapsedTime / 1000,
+ elapsedTime * 100 / totalElapsedTime);
+ timesStr = String.format("%d/%d%%",
+ times,
+ times * 100 / totalTimes);
+ pw.printf(accountFormat, stats.name, timesStr, timeStr);
+ }
+ pw.println(separator);
}
- pw.println(separator);
}
pw.println();
diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java
index bcd599b..e3749b4 100644
--- a/core/java/android/content/pm/ResolveInfo.java
+++ b/core/java/android/content/pm/ResolveInfo.java
@@ -34,8 +34,8 @@ import java.util.Comparator;
*/
public class ResolveInfo implements Parcelable {
/**
- * The activity that corresponds to this resolution match, if this
- * resolution is for an activity. One and only one of this and
+ * The activity or broadcast receiver that corresponds to this resolution match,
+ * if this resolution is for an activity or broadcast receiver. One and only one of this and
* serviceInfo must be non-null.
*/
public ActivityInfo activityInfo;
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 5c3a17a..6015668 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -228,6 +228,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration
public static final int UI_MODE_TYPE_DESK = 0x02;
public static final int UI_MODE_TYPE_CAR = 0x03;
public static final int UI_MODE_TYPE_TELEVISION = 0x04;
+ public static final int UI_MODE_TYPE_APPLIANCE = 0x05;
public static final int UI_MODE_NIGHT_MASK = 0x30;
public static final int UI_MODE_NIGHT_UNDEFINED = 0x00;
@@ -239,7 +240,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration
* <p>The {@link #UI_MODE_TYPE_MASK} bits define the overall ui mode of the
* device. They may be one of {@link #UI_MODE_TYPE_UNDEFINED},
* {@link #UI_MODE_TYPE_NORMAL}, {@link #UI_MODE_TYPE_DESK},
- * or {@link #UI_MODE_TYPE_CAR}.
+ * {@link #UI_MODE_TYPE_CAR}, {@link #UI_MODE_TYPE_TELEVISION}, or
+ * {@link #UI_MODE_TYPE_APPLIANCE}.
*
* <p>The {@link #UI_MODE_NIGHT_MASK} defines whether the screen
* is in a special mode. They may be one of {@link #UI_MODE_NIGHT_UNDEFINED},
@@ -391,6 +393,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration
case UI_MODE_TYPE_DESK: sb.append(" desk"); break;
case UI_MODE_TYPE_CAR: sb.append(" car"); break;
case UI_MODE_TYPE_TELEVISION: sb.append(" television"); break;
+ case UI_MODE_TYPE_APPLIANCE: sb.append(" appliance"); break;
default: sb.append(" uimode="); sb.append(uiMode&UI_MODE_TYPE_MASK); break;
}
switch ((uiMode&UI_MODE_NIGHT_MASK)) {
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index b6b6a8d..2af58be 100755
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -1887,8 +1887,7 @@ public class Resources {
if (cs != null) {
dr = cs.newDrawable(this);
} else {
- if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
- value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
+ if (isColorDrawable) {
dr = new ColorDrawable(value.data);
}
diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java
index 74fef29..b28ed8d 100644
--- a/core/java/android/database/AbstractCursor.java
+++ b/core/java/android/database/AbstractCursor.java
@@ -284,23 +284,6 @@ public abstract class AbstractCursor implements CrossProcessCursor {
}
}
- /**
- * This is hidden until the data set change model has been re-evaluated.
- * @hide
- */
- protected void notifyDataSetChange() {
- mDataSetObservable.notifyChanged();
- }
-
- /**
- * This is hidden until the data set change model has been re-evaluated.
- * @hide
- */
- protected DataSetObservable getDataSetObservable() {
- return mDataSetObservable;
-
- }
-
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
@@ -317,7 +300,7 @@ public abstract class AbstractCursor implements CrossProcessCursor {
*/
protected void onChange(boolean selfChange) {
synchronized (mSelfObserverLock) {
- mContentObservable.dispatchChange(selfChange);
+ mContentObservable.dispatchChange(selfChange, null);
if (mNotifyUri != null && selfChange) {
mContentResolver.notifyChange(mNotifyUri, mSelfObserver);
}
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java
index 20a9c67..67cf0f8 100644
--- a/core/java/android/database/BulkCursorNative.java
+++ b/core/java/android/database/BulkCursorNative.java
@@ -180,13 +180,13 @@ final class BulkCursorProxy implements IBulkCursor {
return mRemote;
}
- public CursorWindow getWindow(int startPos) throws RemoteException
+ public CursorWindow getWindow(int position) throws RemoteException
{
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken(IBulkCursor.descriptor);
- data.writeInt(startPos);
+ data.writeInt(position);
mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0);
DatabaseUtils.readExceptionFromParcel(reply);
diff --git a/core/java/android/database/ContentObservable.java b/core/java/android/database/ContentObservable.java
index 8d7b7c5..7692bb3 100644
--- a/core/java/android/database/ContentObservable.java
+++ b/core/java/android/database/ContentObservable.java
@@ -16,40 +16,75 @@
package android.database;
+import android.net.Uri;
+
/**
- * A specialization of Observable for ContentObserver that provides methods for
- * invoking the various callback methods of ContentObserver.
+ * A specialization of {@link Observable} for {@link ContentObserver}
+ * that provides methods for sending notifications to a list of
+ * {@link ContentObserver} objects.
*/
public class ContentObservable extends Observable<ContentObserver> {
-
+ // Even though the generic method defined in Observable would be perfectly
+ // fine on its own, we can't delete this overridden method because it would
+ // potentially break binary compatibility with existing applications.
@Override
public void registerObserver(ContentObserver observer) {
super.registerObserver(observer);
}
/**
- * invokes dispatchUpdate on each observer, unless the observer doesn't want
- * self-notifications and the update is from a self-notification
- * @param selfChange
+ * Invokes {@link ContentObserver#dispatchChange(boolean)} on each observer.
+ * <p>
+ * If <code>selfChange</code> is true, only delivers the notification
+ * to the observer if it has indicated that it wants to receive self-change
+ * notifications by implementing {@link ContentObserver#deliverSelfNotifications}
+ * to return true.
+ * </p>
+ *
+ * @param selfChange True if this is a self-change notification.
+ *
+ * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead.
*/
+ @Deprecated
public void dispatchChange(boolean selfChange) {
+ dispatchChange(selfChange, null);
+ }
+
+ /**
+ * Invokes {@link ContentObserver#dispatchChange(boolean, Uri)} on each observer.
+ * Includes the changed content Uri when available.
+ * <p>
+ * If <code>selfChange</code> is true, only delivers the notification
+ * to the observer if it has indicated that it wants to receive self-change
+ * notifications by implementing {@link ContentObserver#deliverSelfNotifications}
+ * to return true.
+ * </p>
+ *
+ * @param selfChange True if this is a self-change notification.
+ * @param uri The Uri of the changed content, or null if unknown.
+ */
+ public void dispatchChange(boolean selfChange, Uri uri) {
synchronized(mObservers) {
for (ContentObserver observer : mObservers) {
if (!selfChange || observer.deliverSelfNotifications()) {
- observer.dispatchChange(selfChange);
+ observer.dispatchChange(selfChange, uri);
}
}
}
}
/**
- * invokes onChange on each observer
- * @param selfChange
+ * Invokes {@link ContentObserver#onChange} on each observer.
+ *
+ * @param selfChange True if this is a self-change notification.
+ *
+ * @deprecated Use {@link #dispatchChange} instead.
*/
+ @Deprecated
public void notifyChange(boolean selfChange) {
synchronized(mObservers) {
for (ContentObserver observer : mObservers) {
- observer.onChange(selfChange);
+ observer.onChange(selfChange, null);
}
}
}
diff --git a/core/java/android/database/ContentObserver.java b/core/java/android/database/ContentObserver.java
index 3b829a3..e4fbc28 100644
--- a/core/java/android/database/ContentObserver.java
+++ b/core/java/android/database/ContentObserver.java
@@ -16,65 +16,23 @@
package android.database;
+import android.net.Uri;
import android.os.Handler;
/**
- * Receives call backs for changes to content. Must be implemented by objects which are added
- * to a {@link ContentObservable}.
+ * Receives call backs for changes to content.
+ * Must be implemented by objects which are added to a {@link ContentObservable}.
*/
public abstract class ContentObserver {
+ private final Object mLock = new Object();
+ private Transport mTransport; // guarded by mLock
- private Transport mTransport;
-
- // Protects mTransport
- private Object lock = new Object();
-
- /* package */ Handler mHandler;
-
- private final class NotificationRunnable implements Runnable {
-
- private boolean mSelf;
-
- public NotificationRunnable(boolean self) {
- mSelf = self;
- }
-
- public void run() {
- ContentObserver.this.onChange(mSelf);
- }
- }
-
- private static final class Transport extends IContentObserver.Stub {
- ContentObserver mContentObserver;
-
- public Transport(ContentObserver contentObserver) {
- mContentObserver = contentObserver;
- }
-
- public boolean deliverSelfNotifications() {
- ContentObserver contentObserver = mContentObserver;
- if (contentObserver != null) {
- return contentObserver.deliverSelfNotifications();
- }
- return false;
- }
-
- public void onChange(boolean selfChange) {
- ContentObserver contentObserver = mContentObserver;
- if (contentObserver != null) {
- contentObserver.dispatchChange(selfChange);
- }
- }
-
- public void releaseContentObserver() {
- mContentObserver = null;
- }
- }
+ Handler mHandler;
/**
- * onChange() will happen on the provider Handler.
+ * Creates a content observer.
*
- * @param handler The handler to run {@link #onChange} on.
+ * @param handler The handler to run {@link #onChange} on, or null if none.
*/
public ContentObserver(Handler handler) {
mHandler = handler;
@@ -86,7 +44,7 @@ public abstract class ContentObserver {
* {@hide}
*/
public IContentObserver getContentObserver() {
- synchronized(lock) {
+ synchronized (mLock) {
if (mTransport == null) {
mTransport = new Transport(this);
}
@@ -101,8 +59,8 @@ public abstract class ContentObserver {
* {@hide}
*/
public IContentObserver releaseContentObserver() {
- synchronized(lock) {
- Transport oldTransport = mTransport;
+ synchronized (mLock) {
+ final Transport oldTransport = mTransport;
if (oldTransport != null) {
oldTransport.releaseContentObserver();
mTransport = null;
@@ -112,27 +70,134 @@ public abstract class ContentObserver {
}
/**
- * Returns true if this observer is interested in notifications for changes
- * made through the cursor the observer is registered with.
+ * Returns true if this observer is interested receiving self-change notifications.
+ *
+ * Subclasses should override this method to indicate whether the observer
+ * is interested in receiving notifications for changes that it made to the
+ * content itself.
+ *
+ * @return True if self-change notifications should be delivered to the observer.
*/
public boolean deliverSelfNotifications() {
return false;
}
/**
- * This method is called when a change occurs to the cursor that
- * is being observed.
- *
- * @param selfChange true if the update was caused by a call to <code>commit</code> on the
- * cursor that is being observed.
+ * This method is called when a content change occurs.
+ * <p>
+ * Subclasses should override this method to handle content changes.
+ * </p>
+ *
+ * @param selfChange True if this is a self-change notification.
*/
- public void onChange(boolean selfChange) {}
+ public void onChange(boolean selfChange) {
+ // Do nothing. Subclass should override.
+ }
+ /**
+ * This method is called when a content change occurs.
+ * Includes the changed content Uri when available.
+ * <p>
+ * Subclasses should override this method to handle content changes.
+ * To ensure correct operation on older versions of the framework that
+ * did not provide a Uri argument, applications should also implement
+ * the {@link #onChange(boolean)} overload of this method whenever they
+ * implement the {@link #onChange(boolean, Uri)} overload.
+ * </p><p>
+ * Example implementation:
+ * <pre><code>
+ * // Implement the onChange(boolean) method to delegate the change notification to
+ * // the onChange(boolean, Uri) method to ensure correct operation on older versions
+ * // of the framework that did not have the onChange(boolean, Uri) method.
+ * {@literal @Override}
+ * public void onChange(boolean selfChange) {
+ * onChange(selfChange, null);
+ * }
+ *
+ * // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
+ * {@literal @Override}
+ * public void onChange(boolean selfChange, Uri uri) {
+ * // Handle change.
+ * }
+ * </code></pre>
+ * </p>
+ *
+ * @param selfChange True if this is a self-change notification.
+ * @param uri The Uri of the changed content, or null if unknown.
+ */
+ public void onChange(boolean selfChange, Uri uri) {
+ onChange(selfChange);
+ }
+
+ /**
+ * Dispatches a change notification to the observer.
+ * <p>
+ * If a {@link Handler} was supplied to the {@link ContentObserver} constructor,
+ * then a call to the {@link #onChange} method is posted to the handler's message queue.
+ * Otherwise, the {@link #onChange} method is invoked immediately on this thread.
+ * </p>
+ *
+ * @param selfChange True if this is a self-change notification.
+ *
+ * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead.
+ */
+ @Deprecated
public final void dispatchChange(boolean selfChange) {
+ dispatchChange(selfChange, null);
+ }
+
+ /**
+ * Dispatches a change notification to the observer.
+ * Includes the changed content Uri when available.
+ * <p>
+ * If a {@link Handler} was supplied to the {@link ContentObserver} constructor,
+ * then a call to the {@link #onChange} method is posted to the handler's message queue.
+ * Otherwise, the {@link #onChange} method is invoked immediately on this thread.
+ * </p>
+ *
+ * @param selfChange True if this is a self-change notification.
+ * @param uri The Uri of the changed content, or null if unknown.
+ */
+ public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
- onChange(selfChange);
+ onChange(selfChange, uri);
} else {
- mHandler.post(new NotificationRunnable(selfChange));
+ mHandler.post(new NotificationRunnable(selfChange, uri));
+ }
+ }
+
+ private final class NotificationRunnable implements Runnable {
+ private final boolean mSelfChange;
+ private final Uri mUri;
+
+ public NotificationRunnable(boolean selfChange, Uri uri) {
+ mSelfChange = selfChange;
+ mUri = uri;
+ }
+
+ @Override
+ public void run() {
+ ContentObserver.this.onChange(mSelfChange, mUri);
+ }
+ }
+
+ private static final class Transport extends IContentObserver.Stub {
+ private ContentObserver mContentObserver;
+
+ public Transport(ContentObserver contentObserver) {
+ mContentObserver = contentObserver;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ ContentObserver contentObserver = mContentObserver;
+ if (contentObserver != null) {
+ contentObserver.dispatchChange(selfChange, uri);
+ }
+ }
+
+ public void releaseContentObserver() {
+ mContentObserver = null;
}
}
}
diff --git a/core/java/android/database/CrossProcessCursorWrapper.java b/core/java/android/database/CrossProcessCursorWrapper.java
index 8c250b8..1b77cb9 100644
--- a/core/java/android/database/CrossProcessCursorWrapper.java
+++ b/core/java/android/database/CrossProcessCursorWrapper.java
@@ -24,10 +24,10 @@ import android.database.CursorWrapper;
/**
* Cursor wrapper that implements {@link CrossProcessCursor}.
* <p>
- * If the wrapper cursor implemented {@link CrossProcessCursor}, then delegates
- * {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it. Otherwise,
- * provides default implementations of these methods that traverse the contents
- * of the cursor similar to {@link AbstractCursor#fillWindow}.
+ * If the wrapped cursor implements {@link CrossProcessCursor}, then the wrapper
+ * delegates {@link #fillWindow}, {@link #getWindow()} and {@link #onMove} to it.
+ * Otherwise, the wrapper provides default implementations of these methods that
+ * traverse the contents of the cursor similar to {@link AbstractCursor#fillWindow}.
* </p><p>
* This wrapper can be used to adapt an ordinary {@link Cursor} into a
* {@link CrossProcessCursor}.
diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java
index a9a71cf..59ec89d 100644
--- a/core/java/android/database/Cursor.java
+++ b/core/java/android/database/Cursor.java
@@ -341,6 +341,7 @@ public interface Cursor {
* Deactivates the Cursor, making all calls on it fail until {@link #requery} is called.
* Inactive Cursors use fewer resources than active Cursors.
* Calling {@link #requery} will make the cursor active again.
+ * @deprecated Since {@link #requery()} is deprecated, so too is this.
*/
void deactivate();
diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java
index 215035d..167278a 100644
--- a/core/java/android/database/CursorToBulkCursorAdaptor.java
+++ b/core/java/android/database/CursorToBulkCursorAdaptor.java
@@ -16,6 +16,7 @@
package android.database;
+import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
@@ -78,9 +79,9 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
}
@Override
- public void onChange(boolean selfChange) {
+ public void onChange(boolean selfChange, Uri uri) {
try {
- mRemote.onChange(selfChange);
+ mRemote.onChange(selfChange, uri);
} catch (RemoteException ex) {
// Do nothing, the far side is dead
}
@@ -132,11 +133,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
}
@Override
- public CursorWindow getWindow(int startPos) {
+ public CursorWindow getWindow(int position) {
synchronized (mLock) {
throwIfCursorIsClosed();
- if (!mCursor.moveToPosition(startPos)) {
+ if (!mCursor.moveToPosition(position)) {
closeFilledWindowLocked();
return null;
}
@@ -149,12 +150,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative
if (window == null) {
mFilledWindow = new CursorWindow(mProviderName);
window = mFilledWindow;
- mCursor.fillWindow(startPos, window);
- } else if (startPos < window.getStartPosition()
- || startPos >= window.getStartPosition() + window.getNumRows()) {
+ } else if (position < window.getStartPosition()
+ || position >= window.getStartPosition() + window.getNumRows()) {
window.clear();
- mCursor.fillWindow(startPos, window);
}
+ mCursor.fillWindow(position, window);
}
// Acquire a reference before returning from this RPC.
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index e9675e8..85f570c 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -98,8 +98,8 @@ public class CursorWindow extends SQLiteClosable implements Parcelable {
*/
public CursorWindow(String name) {
mStartPos = 0;
- mName = name;
- mWindowPtr = nativeCreate(name, sCursorWindowSize);
+ mName = name != null && name.length() != 0 ? name : "<unnamed>";
+ mWindowPtr = nativeCreate(mName, sCursorWindowSize);
if (mWindowPtr == 0) {
throw new CursorWindowAllocationException("Cursor window allocation of " +
(sCursorWindowSize / 1024) + " kb failed. " + printStats());
@@ -161,7 +161,7 @@ public class CursorWindow extends SQLiteClosable implements Parcelable {
}
/**
- * Gets the name of this cursor window.
+ * Gets the name of this cursor window, never null.
* @hide
*/
public String getName() {
diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java
index 51c72c1..ca77a13 100644
--- a/core/java/android/database/DataSetObservable.java
+++ b/core/java/android/database/DataSetObservable.java
@@ -17,13 +17,15 @@
package android.database;
/**
- * A specialization of Observable for DataSetObserver that provides methods for
- * invoking the various callback methods of DataSetObserver.
+ * A specialization of {@link Observable} for {@link DataSetObserver}
+ * that provides methods for sending notifications to a list of
+ * {@link DataSetObserver} objects.
*/
public class DataSetObservable extends Observable<DataSetObserver> {
/**
- * Invokes onChanged on each observer. Called when the data set being observed has
- * changed, and which when read contains the new state of the data.
+ * Invokes {@link DataSetObserver#onChanged} on each observer.
+ * Called when the contents of the data set have changed. The recipient
+ * will obtain the new contents the next time it queries the data set.
*/
public void notifyChanged() {
synchronized(mObservers) {
@@ -38,8 +40,9 @@ public class DataSetObservable extends Observable<DataSetObserver> {
}
/**
- * Invokes onInvalidated on each observer. Called when the data set being monitored
- * has changed such that it is no longer valid.
+ * Invokes {@link DataSetObserver#onInvalidated} on each observer.
+ * Called when the data set is no longer valid and cannot be queried again,
+ * such as when the data set has been closed.
*/
public void notifyInvalidated() {
synchronized (mObservers) {
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
index 01bcdf7..b69d9bf 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -727,6 +727,32 @@ public class DatabaseUtils {
}
/**
+ * Picks a start position for {@link Cursor#fillWindow} such that the
+ * window will contain the requested row and a useful range of rows
+ * around it.
+ *
+ * When the data set is too large to fit in a cursor window, seeking the
+ * cursor can become a very expensive operation since we have to run the
+ * query again when we move outside the bounds of the current window.
+ *
+ * We try to choose a start position for the cursor window such that
+ * 1/3 of the window's capacity is used to hold rows before the requested
+ * position and 2/3 of the window's capacity is used to hold rows after the
+ * requested position.
+ *
+ * @param cursorPosition The row index of the row we want to get.
+ * @param cursorWindowCapacity The estimated number of rows that can fit in
+ * a cursor window, or 0 if unknown.
+ * @return The recommended start position, always less than or equal to
+ * the requested row.
+ * @hide
+ */
+ public static int cursorPickFillWindowStartPosition(
+ int cursorPosition, int cursorWindowCapacity) {
+ return Math.max(cursorPosition - cursorWindowCapacity / 3, 0);
+ }
+
+ /**
* Query the table for the number of rows in the table.
* @param db the database the table is in
* @param table the name of the table to query
diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java
index 7c96797..0f4500a 100644
--- a/core/java/android/database/IBulkCursor.java
+++ b/core/java/android/database/IBulkCursor.java
@@ -30,11 +30,17 @@ import android.os.RemoteException;
*/
public interface IBulkCursor extends IInterface {
/**
- * Returns a BulkCursorWindow, which either has a reference to a shared
- * memory segment with the rows, or an array of JSON strings.
+ * Gets a cursor window that contains the specified position.
+ * The window will contain a range of rows around the specified position.
*/
- public CursorWindow getWindow(int startPos) throws RemoteException;
+ public CursorWindow getWindow(int position) throws RemoteException;
+ /**
+ * Notifies the cursor that the position has changed.
+ * Only called when {@link #getWantsAllOnMoveCalls()} returns true.
+ *
+ * @param position The new position
+ */
public void onMove(int position) throws RemoteException;
/**
diff --git a/core/java/android/database/IContentObserver.aidl b/core/java/android/database/IContentObserver.aidl
index ac2f975..13aff05 100755
--- a/core/java/android/database/IContentObserver.aidl
+++ b/core/java/android/database/IContentObserver.aidl
@@ -17,6 +17,8 @@
package android.database;
+import android.net.Uri;
+
/**
* @hide
*/
@@ -27,5 +29,5 @@ interface IContentObserver
* observed. selfUpdate is true if the update was caused by a call to
* commit on the cursor that is being observed.
*/
- oneway void onChange(boolean selfUpdate);
+ oneway void onChange(boolean selfUpdate, in Uri uri);
}
diff --git a/core/java/android/database/Observable.java b/core/java/android/database/Observable.java
index b6fecab..aff32db 100644
--- a/core/java/android/database/Observable.java
+++ b/core/java/android/database/Observable.java
@@ -19,7 +19,12 @@ package android.database;
import java.util.ArrayList;
/**
- * Provides methods for (un)registering arbitrary observers in an ArrayList.
+ * Provides methods for registering or unregistering arbitrary observers in an {@link ArrayList}.
+ *
+ * This abstract class is intended to be subclassed and specialized to maintain
+ * a registry of observers of specific types and dispatch notifications to them.
+ *
+ * @param T The observer type.
*/
public abstract class Observable<T> {
/**
@@ -66,13 +71,13 @@ public abstract class Observable<T> {
mObservers.remove(index);
}
}
-
+
/**
- * Remove all registered observer
+ * Remove all registered observers.
*/
public void unregisterAll() {
synchronized(mObservers) {
mObservers.clear();
- }
+ }
}
}
diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java
deleted file mode 100644
index 39a9d23..0000000
--- a/core/java/android/database/sqlite/DatabaseConnectionPool.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * 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.content.res.Resources;
-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. */
- private volatile int mMaxPoolSize =
- Resources.getSystem().getInteger(com.android.internal.R.integer.db_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 */ synchronized void close() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Closing the connection pool on " + mParentDbObj.getPath() + toString());
- }
- 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 */ synchronized SQLiteDatabase get(String sql) {
- SQLiteDatabase db = null;
- PoolObj poolObj = null;
- 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.isInStatementCache(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.isInStatementCache(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 */ synchronized void release(SQLiteDatabase db) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert db.mConnectionNum > 0;
- doAsserts();
- assert mPool.get(db.mConnectionNum - 1).mDb == db;
- }
-
- PoolObj 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 */ synchronized ArrayList<SQLiteDatabase> getConnectionList() {
- ArrayList<SQLiteDatabase> list = new ArrayList<SQLiteDatabase>();
- 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);
- }
- }
-
- /** only used for testing purposes. */
- /* package */ synchronized void setMaxPoolSize(int size) {
- mMaxPoolSize = size;
- }
-
- /** only used for testing purposes. */
- /* package */ synchronized int getMaxPoolSize() {
- 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/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java
index 01e9fb3..7e91a7b 100644
--- a/core/java/android/database/sqlite/SQLiteClosable.java
+++ b/core/java/android/database/sqlite/SQLiteClosable.java
@@ -16,8 +16,6 @@
package android.database.sqlite;
-import android.database.CursorWindow;
-
/**
* An object created from a SQLiteDatabase that can be closed.
*/
@@ -31,7 +29,7 @@ public abstract class SQLiteClosable {
synchronized(this) {
if (mReferenceCount <= 0) {
throw new IllegalStateException(
- "attempt to re-open an already-closed object: " + getObjInfo());
+ "attempt to re-open an already-closed object: " + this);
}
mReferenceCount++;
}
@@ -56,22 +54,4 @@ public abstract class SQLiteClosable {
onAllReferencesReleasedFromContainer();
}
}
-
- private String getObjInfo() {
- StringBuilder buff = new StringBuilder();
- buff.append(this.getClass().getName());
- buff.append(" (");
- if (this instanceof SQLiteDatabase) {
- buff.append("database = ");
- buff.append(((SQLiteDatabase)this).getPath());
- } else if (this instanceof SQLiteProgram) {
- buff.append("mSql = ");
- buff.append(((SQLiteProgram)this).mSql);
- } else if (this instanceof CursorWindow) {
- buff.append("mStartPos = ");
- buff.append(((CursorWindow)this).getStartPosition());
- }
- buff.append(") ");
- return buff.toString();
- }
}
diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java
deleted file mode 100644
index dafbc79..0000000
--- a/core/java/android/database/sqlite/SQLiteCompiledSql.java
+++ /dev/null
@@ -1,158 +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.database.sqlite;
-
-import android.os.StrictMode;
-import android.util.Log;
-
-/**
- * This class encapsulates compilation of sql statement and release of the compiled statement obj.
- * Once a sql statement is compiled, it is cached in {@link SQLiteDatabase}
- * and it is released in one of the 2 following ways
- * 1. when {@link SQLiteDatabase} object is closed.
- * 2. if this is not cached in {@link SQLiteDatabase}, {@link android.database.Cursor#close()}
- * releaases this obj.
- */
-/* package */ class SQLiteCompiledSql {
-
- private static final String TAG = "SQLiteCompiledSql";
-
- /** The database this program is compiled against. */
- /* package */ final SQLiteDatabase mDatabase;
-
- /**
- * Native linkage, do not modify. This comes from the database.
- */
- /* package */ final int nHandle;
-
- /**
- * Native linkage, do not modify. When non-0 this holds a reference to a valid
- * sqlite3_statement object. It is only updated by the native code, but may be
- * checked in this class when the database lock is held to determine if there
- * is a valid native-side program or not.
- */
- /* package */ int nStatement = 0;
-
- /** the following are for debugging purposes */
- private String mSqlStmt = null;
- private final Throwable mStackTrace;
-
- /** when in cache and is in use, this member is set */
- private boolean mInUse = false;
-
- /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) {
- db.verifyDbIsOpen();
- db.verifyLockOwner();
- mDatabase = db;
- mSqlStmt = sql;
- if (StrictMode.vmSqliteObjectLeaksEnabled()) {
- mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
- } else {
- mStackTrace = null;
- }
- nHandle = db.mNativeHandle;
- native_compile(sql);
- }
-
- /* package */ void releaseSqlStatement() {
- // Note that native_finalize() checks to make sure that nStatement is
- // non-null before destroying it.
- if (nStatement != 0) {
- mDatabase.finalizeStatementLater(nStatement);
- nStatement = 0;
- }
- }
-
- /**
- * returns true if acquire() succeeds. false otherwise.
- */
- /* package */ synchronized boolean acquire() {
- if (mInUse) {
- // it is already in use.
- return false;
- }
- mInUse = true;
- return true;
- }
-
- /* package */ synchronized void release() {
- mInUse = false;
- }
-
- /* package */ synchronized void releaseIfNotInUse() {
- // if it is not in use, release its memory from the database
- if (!mInUse) {
- releaseSqlStatement();
- }
- }
-
- /**
- * Make sure that the native resource is cleaned up.
- */
- @Override
- protected void finalize() throws Throwable {
- try {
- if (nStatement == 0) return;
- // don't worry about finalizing this object if it is ALREADY in the
- // queue of statements to be finalized later
- if (mDatabase.isInQueueOfStatementsToBeFinalized(nStatement)) {
- return;
- }
- // finalizer should NEVER get called
- // but if the database itself is not closed and is GC'ed, then
- // all sub-objects attached to the database could end up getting GC'ed too.
- // in that case, don't print any warning.
- if (mInUse && mStackTrace != null) {
- int len = mSqlStmt.length();
- StrictMode.onSqliteObjectLeaked(
- "Releasing statement in a finalizer. Please ensure " +
- "that you explicitly call close() on your cursor: " +
- mSqlStmt.substring(0, (len > 1000) ? 1000 : len),
- mStackTrace);
- }
- releaseSqlStatement();
- } finally {
- super.finalize();
- }
- }
-
- @Override public String toString() {
- synchronized(this) {
- StringBuilder buff = new StringBuilder();
- buff.append(" nStatement=");
- buff.append(nStatement);
- buff.append(", mInUse=");
- buff.append(mInUse);
- buff.append(", db=");
- buff.append(mDatabase.getPath());
- buff.append(", db_connectionNum=");
- buff.append(mDatabase.mConnectionNum);
- buff.append(", sql=");
- int len = mSqlStmt.length();
- buff.append(mSqlStmt.substring(0, (len > 100) ? 100 : len));
- return buff.toString();
- }
- }
-
- /**
- * Compiles SQL into a SQLite program.
- *
- * <P>The database lock must be held when calling this method.
- * @param sql The SQL to compile.
- */
- private final native void native_compile(String sql);
-}
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
new file mode 100644
index 0000000..710bd53
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -0,0 +1,1315 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import dalvik.system.BlockGuard;
+import dalvik.system.CloseGuard;
+
+import android.content.CancelationSignal;
+import android.content.OperationCanceledException;
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.util.LruCache;
+import android.util.Printer;
+
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a SQLite database connection.
+ * Each connection wraps an instance of a native <code>sqlite3</code> object.
+ * <p>
+ * When database connection pooling is enabled, there can be multiple active
+ * connections to the same database. Otherwise there is typically only one
+ * connection per database.
+ * </p><p>
+ * When the SQLite WAL feature is enabled, multiple readers and one writer
+ * can concurrently access the database. Without WAL, readers and writers
+ * are mutually exclusive.
+ * </p>
+ *
+ * <h2>Ownership and concurrency guarantees</h2>
+ * <p>
+ * Connection objects are not thread-safe. They are acquired as needed to
+ * perform a database operation and are then returned to the pool. At any
+ * given time, a connection is either owned and used by a {@link SQLiteSession}
+ * object or the {@link SQLiteConnectionPool}. Those classes are
+ * responsible for serializing operations to guard against concurrent
+ * use of a connection.
+ * </p><p>
+ * The guarantee of having a single owner allows this class to be implemented
+ * without locks and greatly simplifies resource management.
+ * </p>
+ *
+ * <h2>Encapsulation guarantees</h2>
+ * <p>
+ * The connection object object owns *all* of the SQLite related native
+ * objects that are associated with the connection. What's more, there are
+ * no other objects in the system that are capable of obtaining handles to
+ * those native objects. Consequently, when the connection is closed, we do
+ * not have to worry about what other components might have references to
+ * its associated SQLite state -- there are none.
+ * </p><p>
+ * Encapsulation is what ensures that the connection object's
+ * lifecycle does not become a tortured mess of finalizers and reference
+ * queues.
+ * </p>
+ *
+ * <h2>Reentrance</h2>
+ * <p>
+ * This class must tolerate reentrant execution of SQLite operations because
+ * triggers may call custom SQLite functions that perform additional queries.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteConnection implements CancelationSignal.OnCancelListener {
+ private static final String TAG = "SQLiteConnection";
+ private static final boolean DEBUG = false;
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private static final Pattern TRIM_SQL_PATTERN = Pattern.compile("[\\s]*\\n+[\\s]*");
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final SQLiteConnectionPool mPool;
+ private final SQLiteDatabaseConfiguration mConfiguration;
+ private final int mConnectionId;
+ private final boolean mIsPrimaryConnection;
+ private final PreparedStatementCache mPreparedStatementCache;
+ private PreparedStatement mPreparedStatementPool;
+
+ // The recent operations log.
+ private final OperationLog mRecentOperations = new OperationLog();
+
+ // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY)
+ private int mConnectionPtr;
+
+ private boolean mOnlyAllowReadOnlyOperations;
+
+ // The number of times attachCancelationSignal has been called.
+ // Because SQLite statement execution can be re-entrant, we keep track of how many
+ // times we have attempted to attach a cancelation signal to the connection so that
+ // we can ensure that we detach the signal at the right time.
+ private int mCancelationSignalAttachCount;
+
+ private static native int nativeOpen(String path, int openFlags, String label,
+ boolean enableTrace, boolean enableProfile);
+ private static native void nativeClose(int connectionPtr);
+ private static native void nativeRegisterCustomFunction(int connectionPtr,
+ SQLiteCustomFunction function);
+ private static native void nativeSetLocale(int connectionPtr, String locale);
+ private static native int nativePrepareStatement(int connectionPtr, String sql);
+ private static native void nativeFinalizeStatement(int connectionPtr, int statementPtr);
+ private static native int nativeGetParameterCount(int connectionPtr, int statementPtr);
+ private static native boolean nativeIsReadOnly(int connectionPtr, int statementPtr);
+ private static native int nativeGetColumnCount(int connectionPtr, int statementPtr);
+ private static native String nativeGetColumnName(int connectionPtr, int statementPtr,
+ int index);
+ private static native void nativeBindNull(int connectionPtr, int statementPtr,
+ int index);
+ private static native void nativeBindLong(int connectionPtr, int statementPtr,
+ int index, long value);
+ private static native void nativeBindDouble(int connectionPtr, int statementPtr,
+ int index, double value);
+ private static native void nativeBindString(int connectionPtr, int statementPtr,
+ int index, String value);
+ private static native void nativeBindBlob(int connectionPtr, int statementPtr,
+ int index, byte[] value);
+ private static native void nativeResetStatementAndClearBindings(
+ int connectionPtr, int statementPtr);
+ private static native void nativeExecute(int connectionPtr, int statementPtr);
+ private static native long nativeExecuteForLong(int connectionPtr, int statementPtr);
+ private static native String nativeExecuteForString(int connectionPtr, int statementPtr);
+ private static native int nativeExecuteForBlobFileDescriptor(
+ int connectionPtr, int statementPtr);
+ private static native int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr);
+ private static native long nativeExecuteForLastInsertedRowId(
+ int connectionPtr, int statementPtr);
+ private static native long nativeExecuteForCursorWindow(
+ int connectionPtr, int statementPtr, int windowPtr,
+ int startPos, int requiredPos, boolean countAllRows);
+ private static native int nativeGetDbLookaside(int connectionPtr);
+ private static native void nativeCancel(int connectionPtr);
+ private static native void nativeResetCancel(int connectionPtr, boolean cancelable);
+
+ private SQLiteConnection(SQLiteConnectionPool pool,
+ SQLiteDatabaseConfiguration configuration,
+ int connectionId, boolean primaryConnection) {
+ mPool = pool;
+ mConfiguration = new SQLiteDatabaseConfiguration(configuration);
+ mConnectionId = connectionId;
+ mIsPrimaryConnection = primaryConnection;
+ mPreparedStatementCache = new PreparedStatementCache(
+ mConfiguration.maxSqlCacheSize);
+ mCloseGuard.open("close");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mPool != null && mConnectionPtr != 0) {
+ mPool.onConnectionLeaked();
+ }
+
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ static SQLiteConnection open(SQLiteConnectionPool pool,
+ SQLiteDatabaseConfiguration configuration,
+ int connectionId, boolean primaryConnection) {
+ SQLiteConnection connection = new SQLiteConnection(pool, configuration,
+ connectionId, primaryConnection);
+ try {
+ connection.open();
+ return connection;
+ } catch (SQLiteException ex) {
+ connection.dispose(false);
+ throw ex;
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ // Closes the database closes and releases all of its associated resources.
+ // Do not call methods on the connection after it is closed. It will probably crash.
+ void close() {
+ dispose(false);
+ }
+
+ private void open() {
+ mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags,
+ mConfiguration.label,
+ SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME);
+
+ setLocaleFromConfiguration();
+ }
+
+ private void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (mConnectionPtr != 0) {
+ final int cookie = mRecentOperations.beginOperation("close", null, null);
+ try {
+ mPreparedStatementCache.evictAll();
+ nativeClose(mConnectionPtr);
+ mConnectionPtr = 0;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+ }
+
+ private void setLocaleFromConfiguration() {
+ nativeSetLocale(mConnectionPtr, mConfiguration.locale.toString());
+ }
+
+ // Called by SQLiteConnectionPool only.
+ void reconfigure(SQLiteDatabaseConfiguration configuration) {
+ // Register custom functions.
+ final int functionCount = configuration.customFunctions.size();
+ for (int i = 0; i < functionCount; i++) {
+ SQLiteCustomFunction function = configuration.customFunctions.get(i);
+ if (!mConfiguration.customFunctions.contains(function)) {
+ nativeRegisterCustomFunction(mConnectionPtr, function);
+ }
+ }
+
+ // Remember whether locale has changed.
+ boolean localeChanged = !configuration.locale.equals(mConfiguration.locale);
+
+ // Update configuration parameters.
+ mConfiguration.updateParametersFrom(configuration);
+
+ // Update prepared statement cache size.
+ mPreparedStatementCache.resize(configuration.maxSqlCacheSize);
+
+ // Update locale.
+ if (localeChanged) {
+ setLocaleFromConfiguration();
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ // When set to true, executing write operations will throw SQLiteException.
+ // Preparing statements that might write is ok, just don't execute them.
+ void setOnlyAllowReadOnlyOperations(boolean readOnly) {
+ mOnlyAllowReadOnlyOperations = readOnly;
+ }
+
+ // Called by SQLiteConnectionPool only.
+ // Returns true if the prepared statement cache contains the specified SQL.
+ boolean isPreparedStatementInCache(String sql) {
+ return mPreparedStatementCache.get(sql) != null;
+ }
+
+ /**
+ * Gets the unique id of this connection.
+ * @return The connection id.
+ */
+ public int getConnectionId() {
+ return mConnectionId;
+ }
+
+ /**
+ * Returns true if this is the primary database connection.
+ * @return True if this is the primary database connection.
+ */
+ public boolean isPrimaryConnection() {
+ return mIsPrimaryConnection;
+ }
+
+ /**
+ * Prepares a statement for execution but does not bind its parameters or execute it.
+ * <p>
+ * This method can be used to check for syntax errors during compilation
+ * prior to execution of the statement. If the {@code outStatementInfo} argument
+ * is not null, the provided {@link SQLiteStatementInfo} object is populated
+ * with information about the statement.
+ * </p><p>
+ * A prepared statement makes no reference to the arguments that may eventually
+ * be bound to it, consequently it it possible to cache certain prepared statements
+ * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable,
+ * then it will be stored in the cache for later.
+ * </p><p>
+ * To take advantage of this behavior as an optimization, the connection pool
+ * provides a method to acquire a connection that already has a given SQL statement
+ * in its prepared statement cache so that it is ready for execution.
+ * </p>
+ *
+ * @param sql The SQL statement to prepare.
+ * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+ * with information about the statement, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error.
+ */
+ public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("prepare", sql, null);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ if (outStatementInfo != null) {
+ outStatementInfo.numParameters = statement.mNumParameters;
+ outStatementInfo.readOnly = statement.mReadOnly;
+
+ final int columnCount = nativeGetColumnCount(
+ mConnectionPtr, statement.mStatementPtr);
+ if (columnCount == 0) {
+ outStatementInfo.columnNames = EMPTY_STRING_ARRAY;
+ } else {
+ outStatementInfo.columnNames = new String[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ outStatementInfo.columnNames[i] = nativeGetColumnName(
+ mConnectionPtr, statement.mStatementPtr, i);
+ }
+ }
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement that does not return a result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public void execute(String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ nativeExecute(mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single <code>long</code> result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>long</code>, or zero if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public long executeForLong(String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single {@link String} result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>String</code>, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public String executeForString(String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single BLOB result as a
+ * file descriptor to a shared memory region.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The file descriptor for a shared memory region that contains
+ * the value of the first column in the first row of the result set as a BLOB,
+ * or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor",
+ sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ int fd = nativeExecuteForBlobFileDescriptor(
+ mConnectionPtr, statement.mStatementPtr);
+ return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement that returns a count of the number of rows
+ * that were changed. Use for UPDATE or DELETE SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The number of rows that were changed.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public int executeForChangedRowCount(String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
+ sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ return nativeExecuteForChangedRowCount(
+ mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement that returns the row id of the last row inserted
+ * by the statement. Use for INSERT SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The row id of the last row that was inserted, or 0 if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public long executeForLastInsertedRowId(String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId",
+ sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ return nativeExecuteForLastInsertedRowId(
+ mConnectionPtr, statement.mStatementPtr);
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ mRecentOperations.endOperation(cookie);
+ }
+ }
+
+ /**
+ * Executes a statement and populates the specified {@link CursorWindow}
+ * with a range of results. Returns the number of rows that were counted
+ * during query execution.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param window The cursor window to clear and fill.
+ * @param startPos The start position for filling the window.
+ * @param requiredPos The position of a row that MUST be in the window.
+ * If it won't fit, then the query should discard part of what it filled
+ * so that it does. Must be greater than or equal to <code>startPos</code>.
+ * @param countAllRows True to count all rows that the query would return
+ * regagless of whether they fit in the window.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The number of rows that were counted during query execution. Might
+ * not be all rows in the result set unless <code>countAllRows</code> is true.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public int executeForCursorWindow(String sql, Object[] bindArgs,
+ CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+ if (window == null) {
+ throw new IllegalArgumentException("window must not be null.");
+ }
+
+ int actualPos = -1;
+ int countedRows = -1;
+ int filledRows = -1;
+ final int cookie = mRecentOperations.beginOperation("executeForCursorWindow",
+ sql, bindArgs);
+ try {
+ final PreparedStatement statement = acquirePreparedStatement(sql);
+ try {
+ throwIfStatementForbidden(statement);
+ bindArguments(statement, bindArgs);
+ applyBlockGuardPolicy(statement);
+ attachCancelationSignal(cancelationSignal);
+ try {
+ final long result = nativeExecuteForCursorWindow(
+ mConnectionPtr, statement.mStatementPtr, window.mWindowPtr,
+ startPos, requiredPos, countAllRows);
+ actualPos = (int)(result >> 32);
+ countedRows = (int)result;
+ filledRows = window.getNumRows();
+ window.setStartPosition(actualPos);
+ return countedRows;
+ } finally {
+ detachCancelationSignal(cancelationSignal);
+ }
+ } finally {
+ releasePreparedStatement(statement);
+ }
+ } catch (RuntimeException ex) {
+ mRecentOperations.failOperation(cookie, ex);
+ throw ex;
+ } finally {
+ if (mRecentOperations.endOperationDeferLog(cookie)) {
+ mRecentOperations.logOperation(cookie, "window='" + window
+ + "', startPos=" + startPos
+ + ", actualPos=" + actualPos
+ + ", filledRows=" + filledRows
+ + ", countedRows=" + countedRows);
+ }
+ }
+ }
+
+ private PreparedStatement acquirePreparedStatement(String sql) {
+ PreparedStatement statement = mPreparedStatementCache.get(sql);
+ boolean skipCache = false;
+ if (statement != null) {
+ if (!statement.mInUse) {
+ return statement;
+ }
+ // The statement is already in the cache but is in use (this statement appears
+ // to be not only re-entrant but recursive!). So prepare a new copy of the
+ // statement but do not cache it.
+ skipCache = true;
+ }
+
+ final int statementPtr = nativePrepareStatement(mConnectionPtr, sql);
+ try {
+ final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
+ final int type = DatabaseUtils.getSqlStatementType(sql);
+ final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
+ statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
+ if (!skipCache && isCacheable(type)) {
+ mPreparedStatementCache.put(sql, statement);
+ statement.mInCache = true;
+ }
+ } catch (RuntimeException ex) {
+ // Finalize the statement if an exception occurred and we did not add
+ // it to the cache. If it is already in the cache, then leave it there.
+ if (statement == null || !statement.mInCache) {
+ nativeFinalizeStatement(mConnectionPtr, statementPtr);
+ }
+ throw ex;
+ }
+ statement.mInUse = true;
+ return statement;
+ }
+
+ private void releasePreparedStatement(PreparedStatement statement) {
+ statement.mInUse = false;
+ if (statement.mInCache) {
+ try {
+ nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
+ } catch (SQLiteException ex) {
+ // The statement could not be reset due to an error. Remove it from the cache.
+ // When remove() is called, the cache will invoke its entryRemoved() callback,
+ // which will in turn call finalizePreparedStatement() to finalize and
+ // recycle the statement.
+ if (DEBUG) {
+ Log.d(TAG, "Could not reset prepared statement due to an exception. "
+ + "Removing it from the cache. SQL: "
+ + trimSqlForDisplay(statement.mSql), ex);
+ }
+
+ mPreparedStatementCache.remove(statement.mSql);
+ }
+ } else {
+ finalizePreparedStatement(statement);
+ }
+ }
+
+ private void finalizePreparedStatement(PreparedStatement statement) {
+ nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
+ recyclePreparedStatement(statement);
+ }
+
+ private void attachCancelationSignal(CancelationSignal cancelationSignal) {
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+
+ mCancelationSignalAttachCount += 1;
+ if (mCancelationSignalAttachCount == 1) {
+ // Reset cancelation flag before executing the statement.
+ nativeResetCancel(mConnectionPtr, true /*cancelable*/);
+
+ // After this point, onCancel() may be called concurrently.
+ cancelationSignal.setOnCancelListener(this);
+ }
+ }
+ }
+
+ private void detachCancelationSignal(CancelationSignal cancelationSignal) {
+ if (cancelationSignal != null) {
+ assert mCancelationSignalAttachCount > 0;
+
+ mCancelationSignalAttachCount -= 1;
+ if (mCancelationSignalAttachCount == 0) {
+ // After this point, onCancel() cannot be called concurrently.
+ cancelationSignal.setOnCancelListener(null);
+
+ // Reset cancelation flag after executing the statement.
+ nativeResetCancel(mConnectionPtr, false /*cancelable*/);
+ }
+ }
+ }
+
+ // CancelationSignal.OnCancelationListener callback.
+ // This method may be called on a different thread than the executing statement.
+ // However, it will only be called between calls to attachCancelationSignal and
+ // detachCancelationSignal, while a statement is executing. We can safely assume
+ // that the SQLite connection is still alive.
+ @Override
+ public void onCancel() {
+ nativeCancel(mConnectionPtr);
+ }
+
+ private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
+ final int count = bindArgs != null ? bindArgs.length : 0;
+ if (count != statement.mNumParameters) {
+ throw new SQLiteBindOrColumnIndexOutOfRangeException(
+ "Expected " + statement.mNumParameters + " bind arguments but "
+ + bindArgs.length + " were provided.");
+ }
+ if (count == 0) {
+ return;
+ }
+
+ final int statementPtr = statement.mStatementPtr;
+ for (int i = 0; i < count; i++) {
+ final Object arg = bindArgs[i];
+ switch (DatabaseUtils.getTypeOfObject(arg)) {
+ case Cursor.FIELD_TYPE_NULL:
+ nativeBindNull(mConnectionPtr, statementPtr, i + 1);
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+ ((Number)arg).longValue());
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
+ ((Number)arg).doubleValue());
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ default:
+ if (arg instanceof Boolean) {
+ // Provide compatibility with legacy applications which may pass
+ // Boolean values in bind args.
+ nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+ ((Boolean)arg).booleanValue() ? 1 : 0);
+ } else {
+ nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
+ }
+ break;
+ }
+ }
+ }
+
+ private void throwIfStatementForbidden(PreparedStatement statement) {
+ if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) {
+ throw new SQLiteException("Cannot execute this statement because it "
+ + "might modify the database but the connection is read-only.");
+ }
+ }
+
+ private static boolean isCacheable(int statementType) {
+ if (statementType == DatabaseUtils.STATEMENT_UPDATE
+ || statementType == DatabaseUtils.STATEMENT_SELECT) {
+ return true;
+ }
+ return false;
+ }
+
+ private void applyBlockGuardPolicy(PreparedStatement statement) {
+ if (!mConfiguration.isInMemoryDb()) {
+ if (statement.mReadOnly) {
+ BlockGuard.getThreadPolicy().onReadFromDisk();
+ } else {
+ BlockGuard.getThreadPolicy().onWriteToDisk();
+ }
+ }
+ }
+
+ /**
+ * Dumps debugging information about this connection.
+ *
+ * @param printer The printer to receive the dump, not null.
+ * @param verbose True to dump more verbose information.
+ */
+ public void dump(Printer printer, boolean verbose) {
+ dumpUnsafe(printer, verbose);
+ }
+
+ /**
+ * Dumps debugging information about this connection, in the case where the
+ * caller might not actually own the connection.
+ *
+ * This function is written so that it may be called by a thread that does not
+ * own the connection. We need to be very careful because the connection state is
+ * not synchronized.
+ *
+ * At worst, the method may return stale or slightly wrong data, however
+ * it should not crash. This is ok as it is only used for diagnostic purposes.
+ *
+ * @param printer The printer to receive the dump, not null.
+ * @param verbose True to dump more verbose information.
+ */
+ void dumpUnsafe(Printer printer, boolean verbose) {
+ printer.println("Connection #" + mConnectionId + ":");
+ if (verbose) {
+ printer.println(" connectionPtr: 0x" + Integer.toHexString(mConnectionPtr));
+ }
+ printer.println(" isPrimaryConnection: " + mIsPrimaryConnection);
+ printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
+
+ mRecentOperations.dump(printer);
+
+ if (verbose) {
+ mPreparedStatementCache.dump(printer);
+ }
+ }
+
+ /**
+ * Describes the currently executing operation, in the case where the
+ * caller might not actually own the connection.
+ *
+ * This function is written so that it may be called by a thread that does not
+ * own the connection. We need to be very careful because the connection state is
+ * not synchronized.
+ *
+ * At worst, the method may return stale or slightly wrong data, however
+ * it should not crash. This is ok as it is only used for diagnostic purposes.
+ *
+ * @return A description of the current operation including how long it has been running,
+ * or null if none.
+ */
+ String describeCurrentOperationUnsafe() {
+ return mRecentOperations.describeCurrentOperation();
+ }
+
+ /**
+ * Collects statistics about database connection memory usage.
+ *
+ * @param dbStatsList The list to populate.
+ */
+ void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ // Get information about the main database.
+ int lookaside = nativeGetDbLookaside(mConnectionPtr);
+ long pageCount = 0;
+ long pageSize = 0;
+ try {
+ pageCount = executeForLong("PRAGMA page_count;", null, null);
+ pageSize = executeForLong("PRAGMA page_size;", null, null);
+ } catch (SQLiteException ex) {
+ // Ignore.
+ }
+ dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize));
+
+ // Get information about attached databases.
+ // We ignore the first row in the database list because it corresponds to
+ // the main database which we have already described.
+ CursorWindow window = new CursorWindow("collectDbStats");
+ try {
+ executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false, null);
+ for (int i = 1; i < window.getNumRows(); i++) {
+ String name = window.getString(i, 1);
+ String path = window.getString(i, 2);
+ pageCount = 0;
+ pageSize = 0;
+ try {
+ pageCount = executeForLong("PRAGMA " + name + ".page_count;", null, null);
+ pageSize = executeForLong("PRAGMA " + name + ".page_size;", null, null);
+ } catch (SQLiteException ex) {
+ // Ignore.
+ }
+ String label = " (attached) " + name;
+ if (!path.isEmpty()) {
+ label += ": " + path;
+ }
+ dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0));
+ }
+ } catch (SQLiteException ex) {
+ // Ignore.
+ } finally {
+ window.close();
+ }
+ }
+
+ /**
+ * Collects statistics about database connection memory usage, in the case where the
+ * caller might not actually own the connection.
+ *
+ * @return The statistics object, never null.
+ */
+ void collectDbStatsUnsafe(ArrayList<DbStats> dbStatsList) {
+ dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0));
+ }
+
+ private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) {
+ // The prepared statement cache is thread-safe so we can access its statistics
+ // even if we do not own the database connection.
+ String label = mConfiguration.path;
+ if (!mIsPrimaryConnection) {
+ label += " (" + mConnectionId + ")";
+ }
+ return new DbStats(label, pageCount, pageSize, lookaside,
+ mPreparedStatementCache.hitCount(),
+ mPreparedStatementCache.missCount(),
+ mPreparedStatementCache.size());
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")";
+ }
+
+ private PreparedStatement obtainPreparedStatement(String sql, int statementPtr,
+ int numParameters, int type, boolean readOnly) {
+ PreparedStatement statement = mPreparedStatementPool;
+ if (statement != null) {
+ mPreparedStatementPool = statement.mPoolNext;
+ statement.mPoolNext = null;
+ statement.mInCache = false;
+ } else {
+ statement = new PreparedStatement();
+ }
+ statement.mSql = sql;
+ statement.mStatementPtr = statementPtr;
+ statement.mNumParameters = numParameters;
+ statement.mType = type;
+ statement.mReadOnly = readOnly;
+ return statement;
+ }
+
+ private void recyclePreparedStatement(PreparedStatement statement) {
+ statement.mSql = null;
+ statement.mPoolNext = mPreparedStatementPool;
+ mPreparedStatementPool = statement;
+ }
+
+ private static String trimSqlForDisplay(String sql) {
+ return TRIM_SQL_PATTERN.matcher(sql).replaceAll(" ");
+ }
+
+ /**
+ * Holder type for a prepared statement.
+ *
+ * Although this object holds a pointer to a native statement object, it
+ * does not have a finalizer. This is deliberate. The {@link SQLiteConnection}
+ * owns the statement object and will take care of freeing it when needed.
+ * In particular, closing the connection requires a guarantee of deterministic
+ * resource disposal because all native statement objects must be freed before
+ * the native database object can be closed. So no finalizers here.
+ */
+ private static final class PreparedStatement {
+ // Next item in pool.
+ public PreparedStatement mPoolNext;
+
+ // The SQL from which the statement was prepared.
+ public String mSql;
+
+ // The native sqlite3_stmt object pointer.
+ // Lifetime is managed explicitly by the connection.
+ public int mStatementPtr;
+
+ // The number of parameters that the prepared statement has.
+ public int mNumParameters;
+
+ // The statement type.
+ public int mType;
+
+ // True if the statement is read-only.
+ public boolean mReadOnly;
+
+ // True if the statement is in the cache.
+ public boolean mInCache;
+
+ // True if the statement is in use (currently executing).
+ // We need this flag because due to the use of custom functions in triggers, it's
+ // possible for SQLite calls to be re-entrant. Consequently we need to prevent
+ // in use statements from being finalized until they are no longer in use.
+ public boolean mInUse;
+ }
+
+ private final class PreparedStatementCache
+ extends LruCache<String, PreparedStatement> {
+ public PreparedStatementCache(int size) {
+ super(size);
+ }
+
+ @Override
+ protected void entryRemoved(boolean evicted, String key,
+ PreparedStatement oldValue, PreparedStatement newValue) {
+ oldValue.mInCache = false;
+ if (!oldValue.mInUse) {
+ finalizePreparedStatement(oldValue);
+ }
+ }
+
+ public void dump(Printer printer) {
+ printer.println(" Prepared statement cache:");
+ Map<String, PreparedStatement> cache = snapshot();
+ if (!cache.isEmpty()) {
+ int i = 0;
+ for (Map.Entry<String, PreparedStatement> entry : cache.entrySet()) {
+ PreparedStatement statement = entry.getValue();
+ if (statement.mInCache) { // might be false due to a race with entryRemoved
+ String sql = entry.getKey();
+ printer.println(" " + i + ": statementPtr=0x"
+ + Integer.toHexString(statement.mStatementPtr)
+ + ", numParameters=" + statement.mNumParameters
+ + ", type=" + statement.mType
+ + ", readOnly=" + statement.mReadOnly
+ + ", sql=\"" + trimSqlForDisplay(sql) + "\"");
+ }
+ i += 1;
+ }
+ } else {
+ printer.println(" <none>");
+ }
+ }
+ }
+
+ private static final class OperationLog {
+ private static final int MAX_RECENT_OPERATIONS = 20;
+ private static final int COOKIE_GENERATION_SHIFT = 8;
+ private static final int COOKIE_INDEX_MASK = 0xff;
+
+ private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS];
+ private int mIndex;
+ private int mGeneration;
+
+ public int beginOperation(String kind, String sql, Object[] bindArgs) {
+ synchronized (mOperations) {
+ final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS;
+ Operation operation = mOperations[index];
+ if (operation == null) {
+ operation = new Operation();
+ mOperations[index] = operation;
+ } else {
+ operation.mFinished = false;
+ operation.mException = null;
+ if (operation.mBindArgs != null) {
+ operation.mBindArgs.clear();
+ }
+ }
+ operation.mStartTime = System.currentTimeMillis();
+ operation.mKind = kind;
+ operation.mSql = sql;
+ if (bindArgs != null) {
+ if (operation.mBindArgs == null) {
+ operation.mBindArgs = new ArrayList<Object>();
+ } else {
+ operation.mBindArgs.clear();
+ }
+ for (int i = 0; i < bindArgs.length; i++) {
+ final Object arg = bindArgs[i];
+ if (arg != null && arg instanceof byte[]) {
+ // Don't hold onto the real byte array longer than necessary.
+ operation.mBindArgs.add(EMPTY_BYTE_ARRAY);
+ } else {
+ operation.mBindArgs.add(arg);
+ }
+ }
+ }
+ operation.mCookie = newOperationCookieLocked(index);
+ mIndex = index;
+ return operation.mCookie;
+ }
+ }
+
+ public void failOperation(int cookie, Exception ex) {
+ synchronized (mOperations) {
+ final Operation operation = getOperationLocked(cookie);
+ if (operation != null) {
+ operation.mException = ex;
+ }
+ }
+ }
+
+ public void endOperation(int cookie) {
+ synchronized (mOperations) {
+ if (endOperationDeferLogLocked(cookie)) {
+ logOperationLocked(cookie, null);
+ }
+ }
+ }
+
+ public boolean endOperationDeferLog(int cookie) {
+ synchronized (mOperations) {
+ return endOperationDeferLogLocked(cookie);
+ }
+ }
+
+ public void logOperation(int cookie, String detail) {
+ synchronized (mOperations) {
+ logOperationLocked(cookie, detail);
+ }
+ }
+
+ private boolean endOperationDeferLogLocked(int cookie) {
+ final Operation operation = getOperationLocked(cookie);
+ if (operation != null) {
+ operation.mEndTime = System.currentTimeMillis();
+ operation.mFinished = true;
+ return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery(
+ operation.mEndTime - operation.mStartTime);
+ }
+ return false;
+ }
+
+ private void logOperationLocked(int cookie, String detail) {
+ final Operation operation = getOperationLocked(cookie);
+ StringBuilder msg = new StringBuilder();
+ operation.describe(msg);
+ if (detail != null) {
+ msg.append(", ").append(detail);
+ }
+ Log.d(TAG, msg.toString());
+ }
+
+ private int newOperationCookieLocked(int index) {
+ final int generation = mGeneration++;
+ return generation << COOKIE_GENERATION_SHIFT | index;
+ }
+
+ private Operation getOperationLocked(int cookie) {
+ final int index = cookie & COOKIE_INDEX_MASK;
+ final Operation operation = mOperations[index];
+ return operation.mCookie == cookie ? operation : null;
+ }
+
+ public String describeCurrentOperation() {
+ synchronized (mOperations) {
+ final Operation operation = mOperations[mIndex];
+ if (operation != null && !operation.mFinished) {
+ StringBuilder msg = new StringBuilder();
+ operation.describe(msg);
+ return msg.toString();
+ }
+ return null;
+ }
+ }
+
+ public void dump(Printer printer) {
+ synchronized (mOperations) {
+ printer.println(" Most recently executed operations:");
+ int index = mIndex;
+ Operation operation = mOperations[index];
+ if (operation != null) {
+ int n = 0;
+ do {
+ StringBuilder msg = new StringBuilder();
+ msg.append(" ").append(n).append(": [");
+ msg.append(operation.getFormattedStartTime());
+ msg.append("] ");
+ operation.describe(msg);
+ printer.println(msg.toString());
+
+ if (index > 0) {
+ index -= 1;
+ } else {
+ index = MAX_RECENT_OPERATIONS - 1;
+ }
+ n += 1;
+ operation = mOperations[index];
+ } while (operation != null && n < MAX_RECENT_OPERATIONS);
+ } else {
+ printer.println(" <none>");
+ }
+ }
+ }
+ }
+
+ private static final class Operation {
+ private static final SimpleDateFormat sDateFormat =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ public long mStartTime;
+ public long mEndTime;
+ public String mKind;
+ public String mSql;
+ public ArrayList<Object> mBindArgs;
+ public boolean mFinished;
+ public Exception mException;
+ public int mCookie;
+
+ public void describe(StringBuilder msg) {
+ msg.append(mKind);
+ if (mFinished) {
+ msg.append(" took ").append(mEndTime - mStartTime).append("ms");
+ } else {
+ msg.append(" started ").append(System.currentTimeMillis() - mStartTime)
+ .append("ms ago");
+ }
+ msg.append(" - ").append(getStatus());
+ if (mSql != null) {
+ msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\"");
+ }
+ if (mBindArgs != null && mBindArgs.size() != 0) {
+ msg.append(", bindArgs=[");
+ final int count = mBindArgs.size();
+ for (int i = 0; i < count; i++) {
+ final Object arg = mBindArgs.get(i);
+ if (i != 0) {
+ msg.append(", ");
+ }
+ if (arg == null) {
+ msg.append("null");
+ } else if (arg instanceof byte[]) {
+ msg.append("<byte[]>");
+ } else if (arg instanceof String) {
+ msg.append("\"").append((String)arg).append("\"");
+ } else {
+ msg.append(arg);
+ }
+ }
+ msg.append("]");
+ }
+ if (mException != null) {
+ msg.append(", exception=\"").append(mException.getMessage()).append("\"");
+ }
+ }
+
+ private String getStatus() {
+ if (!mFinished) {
+ return "running";
+ }
+ return mException != null ? "failed" : "succeeded";
+ }
+
+ private String getFormattedStartTime() {
+ return sDateFormat.format(new Date(mStartTime));
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
new file mode 100644
index 0000000..d335738
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -0,0 +1,970 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import dalvik.system.CloseGuard;
+
+import android.content.CancelationSignal;
+import android.content.OperationCanceledException;
+import android.database.sqlite.SQLiteDebug.DbStats;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.PrefixPrinter;
+import android.util.Printer;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * Maintains a pool of active SQLite database connections.
+ * <p>
+ * At any given time, a connection is either owned by the pool, or it has been
+ * acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is
+ * finished with the connection it is using, it must return the connection
+ * back to the pool.
+ * </p><p>
+ * The pool holds strong references to the connections it owns. However,
+ * it only holds <em>weak references</em> to the connections that sessions
+ * have acquired from it. Using weak references in the latter case ensures
+ * that the connection pool can detect when connections have been improperly
+ * abandoned so that it can create new connections to replace them if needed.
+ * </p><p>
+ * The connection pool is thread-safe (but the connections themselves are not).
+ * </p>
+ *
+ * <h2>Exception safety</h2>
+ * <p>
+ * This code attempts to maintain the invariant that opened connections are
+ * always owned. Unfortunately that means it needs to handle exceptions
+ * all over to ensure that broken connections get cleaned up. Most
+ * operations invokving SQLite can throw {@link SQLiteException} or other
+ * runtime exceptions. This is a bit of a pain to deal with because the compiler
+ * cannot help us catch missing exception handling code.
+ * </p><p>
+ * The general rule for this file: If we are making calls out to
+ * {@link SQLiteConnection} then we must be prepared to handle any
+ * runtime exceptions it might throw at us. Note that out-of-memory
+ * is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves
+ * handling out of memory because it is hard to do anything at all sensible then
+ * and most likely the VM is about to crash.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteConnectionPool implements Closeable {
+ private static final String TAG = "SQLiteConnectionPool";
+
+ // Amount of time to wait in milliseconds before unblocking acquireConnection
+ // and logging a message about the connection pool being busy.
+ private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final Object mLock = new Object();
+ private final AtomicBoolean mConnectionLeaked = new AtomicBoolean();
+ private final SQLiteDatabaseConfiguration mConfiguration;
+ private boolean mIsOpen;
+ private int mNextConnectionId;
+
+ private ConnectionWaiter mConnectionWaiterPool;
+ private ConnectionWaiter mConnectionWaiterQueue;
+
+ // Strong references to all available connections.
+ private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections =
+ new ArrayList<SQLiteConnection>();
+ private SQLiteConnection mAvailablePrimaryConnection;
+
+ // Weak references to all acquired connections. The associated value
+ // is a boolean that indicates whether the connection must be reconfigured
+ // before being returned to the available connection list.
+ // For example, the prepared statement cache size may have changed and
+ // need to be updated.
+ private final WeakHashMap<SQLiteConnection, Boolean> mAcquiredConnections =
+ new WeakHashMap<SQLiteConnection, Boolean>();
+
+ /**
+ * Connection flag: Read-only.
+ * <p>
+ * This flag indicates that the connection will only be used to
+ * perform read-only operations.
+ * </p>
+ */
+ public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0;
+
+ /**
+ * Connection flag: Primary connection affinity.
+ * <p>
+ * This flag indicates that the primary connection is required.
+ * This flag helps support legacy applications that expect most data modifying
+ * operations to be serialized by locking the primary database connection.
+ * Setting this flag essentially implements the old "db lock" concept by preventing
+ * an operation from being performed until it can obtain exclusive access to
+ * the primary connection.
+ * </p>
+ */
+ public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1;
+
+ /**
+ * Connection flag: Connection is being used interactively.
+ * <p>
+ * This flag indicates that the connection is needed by the UI thread.
+ * The connection pool can use this flag to elevate the priority
+ * of the database connection request.
+ * </p>
+ */
+ public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2;
+
+ private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
+ mConfiguration = new SQLiteDatabaseConfiguration(configuration);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Opens a connection pool for the specified database.
+ *
+ * @param configuration The database configuration.
+ * @return The connection pool.
+ *
+ * @throws SQLiteException if a database error occurs.
+ */
+ public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) {
+ if (configuration == null) {
+ throw new IllegalArgumentException("configuration must not be null.");
+ }
+
+ // Create the pool.
+ SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration);
+ pool.open(); // might throw
+ return pool;
+ }
+
+ // Might throw
+ private void open() {
+ // Open the primary connection.
+ // This might throw if the database is corrupt.
+ mAvailablePrimaryConnection = openConnectionLocked(
+ true /*primaryConnection*/); // might throw
+
+ // Mark the pool as being open for business.
+ mIsOpen = true;
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Closes the connection pool.
+ * <p>
+ * When the connection pool is closed, it will refuse all further requests
+ * to acquire connections. All connections that are currently available in
+ * the pool are closed immediately. Any connections that are still in use
+ * will be closed as soon as they are returned to the pool.
+ * </p>
+ *
+ * @throws IllegalStateException if the pool has been closed.
+ */
+ public void close() {
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ if (mCloseGuard != null) {
+ if (finalized) {
+ mCloseGuard.warnIfOpen();
+ }
+ mCloseGuard.close();
+ }
+
+ if (!finalized) {
+ // Close all connections. We don't need (or want) to do this
+ // when finalized because we don't know what state the connections
+ // themselves will be in. The finalizer is really just here for CloseGuard.
+ // The connections will take care of themselves when their own finalizers run.
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ mIsOpen = false;
+
+ final int count = mAvailableNonPrimaryConnections.size();
+ for (int i = 0; i < count; i++) {
+ closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i));
+ }
+ mAvailableNonPrimaryConnections.clear();
+
+ if (mAvailablePrimaryConnection != null) {
+ closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+ mAvailablePrimaryConnection = null;
+ }
+
+ final int pendingCount = mAcquiredConnections.size();
+ if (pendingCount != 0) {
+ Log.i(TAG, "The connection pool for " + mConfiguration.label
+ + " has been closed but there are still "
+ + pendingCount + " connections in use. They will be closed "
+ + "as they are released back to the pool.");
+ }
+
+ wakeConnectionWaitersLocked();
+ }
+ }
+ }
+
+ /**
+ * Reconfigures the database configuration of the connection pool and all of its
+ * connections.
+ * <p>
+ * Configuration changes are propagated down to connections immediately if
+ * they are available or as soon as they are released. This includes changes
+ * that affect the size of the pool.
+ * </p>
+ *
+ * @param configuration The new configuration.
+ *
+ * @throws IllegalStateException if the pool has been closed.
+ */
+ public void reconfigure(SQLiteDatabaseConfiguration configuration) {
+ if (configuration == null) {
+ throw new IllegalArgumentException("configuration must not be null.");
+ }
+
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ final boolean poolSizeChanged = mConfiguration.maxConnectionPoolSize
+ != configuration.maxConnectionPoolSize;
+ mConfiguration.updateParametersFrom(configuration);
+
+ if (poolSizeChanged) {
+ int availableCount = mAvailableNonPrimaryConnections.size();
+ while (availableCount-- > mConfiguration.maxConnectionPoolSize - 1) {
+ SQLiteConnection connection =
+ mAvailableNonPrimaryConnections.remove(availableCount);
+ closeConnectionAndLogExceptionsLocked(connection);
+ }
+ }
+
+ reconfigureAllConnectionsLocked();
+
+ wakeConnectionWaitersLocked();
+ }
+ }
+
+ /**
+ * Acquires a connection from the pool.
+ * <p>
+ * The caller must call {@link #releaseConnection} to release the connection
+ * back to the pool when it is finished. Failure to do so will result
+ * in much unpleasantness.
+ * </p>
+ *
+ * @param sql If not null, try to find a connection that already has
+ * the specified SQL statement in its prepared statement cache.
+ * @param connectionFlags The connection request flags.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The connection that was acquired, never null.
+ *
+ * @throws IllegalStateException if the pool has been closed.
+ * @throws SQLiteException if a database error occurs.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public SQLiteConnection acquireConnection(String sql, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ return waitForConnection(sql, connectionFlags, cancelationSignal);
+ }
+
+ /**
+ * Releases a connection back to the pool.
+ * <p>
+ * It is ok to call this method after the pool has closed, to release
+ * connections that were still in use at the time of closure.
+ * </p>
+ *
+ * @param connection The connection to release. Must not be null.
+ *
+ * @throws IllegalStateException if the connection was not acquired
+ * from this pool or if it has already been released.
+ */
+ public void releaseConnection(SQLiteConnection connection) {
+ synchronized (mLock) {
+ Boolean mustReconfigure = mAcquiredConnections.remove(connection);
+ if (mustReconfigure == null) {
+ throw new IllegalStateException("Cannot perform this operation "
+ + "because the specified connection was not acquired "
+ + "from this pool or has already been released.");
+ }
+
+ if (!mIsOpen) {
+ closeConnectionAndLogExceptionsLocked(connection);
+ } else if (connection.isPrimaryConnection()) {
+ assert mAvailablePrimaryConnection == null;
+ try {
+ if (mustReconfigure == Boolean.TRUE) {
+ connection.reconfigure(mConfiguration); // might throw
+ }
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure released primary connection, closing it: "
+ + connection, ex);
+ closeConnectionAndLogExceptionsLocked(connection);
+ connection = null;
+ }
+ if (connection != null) {
+ mAvailablePrimaryConnection = connection;
+ }
+ wakeConnectionWaitersLocked();
+ } else if (mAvailableNonPrimaryConnections.size() >=
+ mConfiguration.maxConnectionPoolSize - 1) {
+ closeConnectionAndLogExceptionsLocked(connection);
+ } else {
+ try {
+ if (mustReconfigure == Boolean.TRUE) {
+ connection.reconfigure(mConfiguration); // might throw
+ }
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure released non-primary connection, "
+ + "closing it: " + connection, ex);
+ closeConnectionAndLogExceptionsLocked(connection);
+ connection = null;
+ }
+ if (connection != null) {
+ mAvailableNonPrimaryConnections.add(connection);
+ }
+ wakeConnectionWaitersLocked();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the session should yield the connection due to
+ * contention over available database connections.
+ *
+ * @param connection The connection owned by the session.
+ * @param connectionFlags The connection request flags.
+ * @return True if the session should yield its connection.
+ *
+ * @throws IllegalStateException if the connection was not acquired
+ * from this pool or if it has already been released.
+ */
+ public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) {
+ synchronized (mLock) {
+ if (!mAcquiredConnections.containsKey(connection)) {
+ throw new IllegalStateException("Cannot perform this operation "
+ + "because the specified connection was not acquired "
+ + "from this pool or has already been released.");
+ }
+
+ if (!mIsOpen) {
+ return false;
+ }
+
+ return isSessionBlockingImportantConnectionWaitersLocked(
+ connection.isPrimaryConnection(), connectionFlags);
+ }
+ }
+
+ /**
+ * Collects statistics about database connection memory usage.
+ *
+ * @param dbStatsList The list to populate.
+ */
+ public void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ synchronized (mLock) {
+ if (mAvailablePrimaryConnection != null) {
+ mAvailablePrimaryConnection.collectDbStats(dbStatsList);
+ }
+
+ for (SQLiteConnection connection : mAvailableNonPrimaryConnections) {
+ connection.collectDbStats(dbStatsList);
+ }
+
+ for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
+ connection.collectDbStatsUnsafe(dbStatsList);
+ }
+ }
+ }
+
+ // Might throw.
+ private SQLiteConnection openConnectionLocked(boolean primaryConnection) {
+ final int connectionId = mNextConnectionId++;
+ return SQLiteConnection.open(this, mConfiguration,
+ connectionId, primaryConnection); // might throw
+ }
+
+ void onConnectionLeaked() {
+ // This code is running inside of the SQLiteConnection finalizer.
+ //
+ // We don't know whether it is just the connection that has been finalized (and leaked)
+ // or whether the connection pool has also been or is about to be finalized.
+ // Consequently, it would be a bad idea to try to grab any locks or to
+ // do any significant work here. So we do the simplest possible thing and
+ // set a flag. waitForConnection() periodically checks this flag (when it
+ // times out) so that it can recover from leaked connections and wake
+ // itself or other threads up if necessary.
+ //
+ // You might still wonder why we don't try to do more to wake up the waiters
+ // immediately. First, as explained above, it would be hard to do safely
+ // unless we started an extra Thread to function as a reference queue. Second,
+ // this is never supposed to happen in normal operation. Third, there is no
+ // guarantee that the GC will actually detect the leak in a timely manner so
+ // it's not all that important that we recover from the leak in a timely manner
+ // either. Fourth, if a badly behaved application finds itself hung waiting for
+ // several seconds while waiting for a leaked connection to be detected and recreated,
+ // then perhaps its authors will have added incentive to fix the problem!
+
+ Log.w(TAG, "A SQLiteConnection object for database '"
+ + mConfiguration.label + "' was leaked! Please fix your application "
+ + "to end transactions in progress properly and to close the database "
+ + "when it is no longer needed.");
+
+ mConnectionLeaked.set(true);
+ }
+
+ // Can't throw.
+ private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) {
+ try {
+ connection.close(); // might throw
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to close connection, its fate is now in the hands "
+ + "of the merciful GC: " + connection, ex);
+ }
+ }
+
+ // Can't throw.
+ private void reconfigureAllConnectionsLocked() {
+ boolean wake = false;
+ if (mAvailablePrimaryConnection != null) {
+ try {
+ mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure available primary connection, closing it: "
+ + mAvailablePrimaryConnection, ex);
+ closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection);
+ mAvailablePrimaryConnection = null;
+ wake = true;
+ }
+ }
+
+ int count = mAvailableNonPrimaryConnections.size();
+ for (int i = 0; i < count; i++) {
+ final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i);
+ try {
+ connection.reconfigure(mConfiguration); // might throw
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: "
+ + connection, ex);
+ closeConnectionAndLogExceptionsLocked(connection);
+ mAvailableNonPrimaryConnections.remove(i--);
+ count -= 1;
+ wake = true;
+ }
+ }
+
+ if (!mAcquiredConnections.isEmpty()) {
+ ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>(
+ mAcquiredConnections.size());
+ for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) {
+ if (entry.getValue() != Boolean.TRUE) {
+ keysToUpdate.add(entry.getKey());
+ }
+ }
+ final int updateCount = keysToUpdate.size();
+ for (int i = 0; i < updateCount; i++) {
+ mAcquiredConnections.put(keysToUpdate.get(i), Boolean.TRUE);
+ }
+ }
+
+ if (wake) {
+ wakeConnectionWaitersLocked();
+ }
+ }
+
+ // Might throw.
+ private SQLiteConnection waitForConnection(String sql, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ final boolean wantPrimaryConnection =
+ (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0;
+
+ final ConnectionWaiter waiter;
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ // Abort if canceled.
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ }
+
+ // Try to acquire a connection.
+ SQLiteConnection connection = null;
+ if (!wantPrimaryConnection) {
+ connection = tryAcquireNonPrimaryConnectionLocked(
+ sql, connectionFlags); // might throw
+ }
+ if (connection == null) {
+ connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw
+ }
+ if (connection != null) {
+ return connection;
+ }
+
+ // No connections available. Enqueue a waiter in priority order.
+ final int priority = getPriority(connectionFlags);
+ final long startTime = SystemClock.uptimeMillis();
+ waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
+ priority, wantPrimaryConnection, sql, connectionFlags);
+ ConnectionWaiter predecessor = null;
+ ConnectionWaiter successor = mConnectionWaiterQueue;
+ while (successor != null) {
+ if (priority > successor.mPriority) {
+ waiter.mNext = successor;
+ break;
+ }
+ predecessor = successor;
+ successor = successor.mNext;
+ }
+ if (predecessor != null) {
+ predecessor.mNext = waiter;
+ } else {
+ mConnectionWaiterQueue = waiter;
+ }
+
+ if (cancelationSignal != null) {
+ final int nonce = waiter.mNonce;
+ cancelationSignal.setOnCancelListener(new CancelationSignal.OnCancelListener() {
+ @Override
+ public void onCancel() {
+ synchronized (mLock) {
+ cancelConnectionWaiterLocked(waiter, nonce);
+ }
+ }
+ });
+ }
+ }
+
+ // Park the thread until a connection is assigned or the pool is closed.
+ // Rethrow an exception from the wait, if we got one.
+ long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
+ long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
+ for (;;) {
+ // Detect and recover from connection leaks.
+ if (mConnectionLeaked.compareAndSet(true, false)) {
+ synchronized (mLock) {
+ wakeConnectionWaitersLocked();
+ }
+ }
+
+ // Wait to be unparked (may already have happened), a timeout, or interruption.
+ LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);
+
+ // Clear the interrupted flag, just in case.
+ Thread.interrupted();
+
+ // Check whether we are done waiting yet.
+ synchronized (mLock) {
+ throwIfClosedLocked();
+
+ final SQLiteConnection connection = waiter.mAssignedConnection;
+ final RuntimeException ex = waiter.mException;
+ if (connection != null || ex != null) {
+ if (cancelationSignal != null) {
+ cancelationSignal.setOnCancelListener(null);
+ }
+ recycleConnectionWaiterLocked(waiter);
+ if (connection != null) {
+ return connection;
+ }
+ throw ex; // rethrow!
+ }
+
+ final long now = SystemClock.uptimeMillis();
+ if (now < nextBusyTimeoutTime) {
+ busyTimeoutMillis = now - nextBusyTimeoutTime;
+ } else {
+ logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags);
+ busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
+ nextBusyTimeoutTime = now + busyTimeoutMillis;
+ }
+ }
+ }
+ }
+
+ // Can't throw.
+ private void cancelConnectionWaiterLocked(ConnectionWaiter waiter, int nonce) {
+ if (waiter.mNonce != nonce) {
+ // Waiter already removed and recycled.
+ return;
+ }
+
+ if (waiter.mAssignedConnection != null || waiter.mException != null) {
+ // Waiter is done waiting but has not woken up yet.
+ return;
+ }
+
+ // Waiter must still be waiting. Dequeue it.
+ ConnectionWaiter predecessor = null;
+ ConnectionWaiter current = mConnectionWaiterQueue;
+ while (current != waiter) {
+ assert current != null;
+ predecessor = current;
+ current = current.mNext;
+ }
+ if (predecessor != null) {
+ predecessor.mNext = waiter.mNext;
+ } else {
+ mConnectionWaiterQueue = waiter.mNext;
+ }
+
+ // Send the waiter an exception and unpark it.
+ waiter.mException = new OperationCanceledException();
+ LockSupport.unpark(waiter.mThread);
+
+ // Check whether removing this waiter will enable other waiters to make progress.
+ wakeConnectionWaitersLocked();
+ }
+
+ // Can't throw.
+ private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) {
+ final Thread thread = Thread.currentThread();
+ StringBuilder msg = new StringBuilder();
+ msg.append("The connection pool for database '").append(mConfiguration.label);
+ msg.append("' has been unable to grant a connection to thread ");
+ msg.append(thread.getId()).append(" (").append(thread.getName()).append(") ");
+ msg.append("with flags 0x").append(Integer.toHexString(connectionFlags));
+ msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n");
+
+ ArrayList<String> requests = new ArrayList<String>();
+ int activeConnections = 0;
+ int idleConnections = 0;
+ if (!mAcquiredConnections.isEmpty()) {
+ for (Map.Entry<SQLiteConnection, Boolean> entry : mAcquiredConnections.entrySet()) {
+ final SQLiteConnection connection = entry.getKey();
+ String description = connection.describeCurrentOperationUnsafe();
+ if (description != null) {
+ requests.add(description);
+ activeConnections += 1;
+ } else {
+ idleConnections += 1;
+ }
+ }
+ }
+ int availableConnections = mAvailableNonPrimaryConnections.size();
+ if (mAvailablePrimaryConnection != null) {
+ availableConnections += 1;
+ }
+
+ msg.append("Connections: ").append(activeConnections).append(" active, ");
+ msg.append(idleConnections).append(" idle, ");
+ msg.append(availableConnections).append(" available.\n");
+
+ if (!requests.isEmpty()) {
+ msg.append("\nRequests in progress:\n");
+ for (String request : requests) {
+ msg.append(" ").append(request).append("\n");
+ }
+ }
+
+ Log.w(TAG, msg.toString());
+ }
+
+ // Can't throw.
+ private void wakeConnectionWaitersLocked() {
+ // Unpark all waiters that have requests that we can fulfill.
+ // This method is designed to not throw runtime exceptions, although we might send
+ // a waiter an exception for it to rethrow.
+ ConnectionWaiter predecessor = null;
+ ConnectionWaiter waiter = mConnectionWaiterQueue;
+ boolean primaryConnectionNotAvailable = false;
+ boolean nonPrimaryConnectionNotAvailable = false;
+ while (waiter != null) {
+ boolean unpark = false;
+ if (!mIsOpen) {
+ unpark = true;
+ } else {
+ try {
+ SQLiteConnection connection = null;
+ if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) {
+ connection = tryAcquireNonPrimaryConnectionLocked(
+ waiter.mSql, waiter.mConnectionFlags); // might throw
+ if (connection == null) {
+ nonPrimaryConnectionNotAvailable = true;
+ }
+ }
+ if (connection == null && !primaryConnectionNotAvailable) {
+ connection = tryAcquirePrimaryConnectionLocked(
+ waiter.mConnectionFlags); // might throw
+ if (connection == null) {
+ primaryConnectionNotAvailable = true;
+ }
+ }
+ if (connection != null) {
+ waiter.mAssignedConnection = connection;
+ unpark = true;
+ } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) {
+ // There are no connections available and the pool is still open.
+ // We cannot fulfill any more connection requests, so stop here.
+ break;
+ }
+ } catch (RuntimeException ex) {
+ // Let the waiter handle the exception from acquiring a connection.
+ waiter.mException = ex;
+ unpark = true;
+ }
+ }
+
+ final ConnectionWaiter successor = waiter.mNext;
+ if (unpark) {
+ if (predecessor != null) {
+ predecessor.mNext = successor;
+ } else {
+ mConnectionWaiterQueue = successor;
+ }
+ waiter.mNext = null;
+
+ LockSupport.unpark(waiter.mThread);
+ } else {
+ predecessor = waiter;
+ }
+ waiter = successor;
+ }
+ }
+
+ // Might throw.
+ private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) {
+ // If the primary connection is available, acquire it now.
+ SQLiteConnection connection = mAvailablePrimaryConnection;
+ if (connection != null) {
+ mAvailablePrimaryConnection = null;
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Make sure that the primary connection actually exists and has just been acquired.
+ for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) {
+ if (acquiredConnection.isPrimaryConnection()) {
+ return null;
+ }
+ }
+
+ // Uhoh. No primary connection! Either this is the first time we asked
+ // for it, or maybe it leaked?
+ connection = openConnectionLocked(true /*primaryConnection*/); // might throw
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Might throw.
+ private SQLiteConnection tryAcquireNonPrimaryConnectionLocked(
+ String sql, int connectionFlags) {
+ // Try to acquire the next connection in the queue.
+ SQLiteConnection connection;
+ final int availableCount = mAvailableNonPrimaryConnections.size();
+ if (availableCount > 1 && sql != null) {
+ // If we have a choice, then prefer a connection that has the
+ // prepared statement in its cache.
+ for (int i = 0; i < availableCount; i++) {
+ connection = mAvailableNonPrimaryConnections.get(i);
+ if (connection.isPreparedStatementInCache(sql)) {
+ mAvailableNonPrimaryConnections.remove(i);
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+ }
+ }
+ if (availableCount > 0) {
+ // Otherwise, just grab the next one.
+ connection = mAvailableNonPrimaryConnections.remove(availableCount - 1);
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Expand the pool if needed.
+ int openConnections = mAcquiredConnections.size();
+ if (mAvailablePrimaryConnection != null) {
+ openConnections += 1;
+ }
+ if (openConnections >= mConfiguration.maxConnectionPoolSize) {
+ return null;
+ }
+ connection = openConnectionLocked(false /*primaryConnection*/); // might throw
+ finishAcquireConnectionLocked(connection, connectionFlags); // might throw
+ return connection;
+ }
+
+ // Might throw.
+ private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) {
+ try {
+ final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0;
+ connection.setOnlyAllowReadOnlyOperations(readOnly);
+
+ mAcquiredConnections.put(connection, Boolean.FALSE);
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Failed to prepare acquired connection for session, closing it: "
+ + connection +", connectionFlags=" + connectionFlags);
+ closeConnectionAndLogExceptionsLocked(connection);
+ throw ex; // rethrow!
+ }
+ }
+
+ private boolean isSessionBlockingImportantConnectionWaitersLocked(
+ boolean holdingPrimaryConnection, int connectionFlags) {
+ ConnectionWaiter waiter = mConnectionWaiterQueue;
+ if (waiter != null) {
+ final int priority = getPriority(connectionFlags);
+ do {
+ // Only worry about blocked connections that have same or lower priority.
+ if (priority > waiter.mPriority) {
+ break;
+ }
+
+ // If we are holding the primary connection then we are blocking the waiter.
+ // Likewise, if we are holding a non-primary connection and the waiter
+ // would accept a non-primary connection, then we are blocking the waier.
+ if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) {
+ return true;
+ }
+
+ waiter = waiter.mNext;
+ } while (waiter != null);
+ }
+ return false;
+ }
+
+ private static int getPriority(int connectionFlags) {
+ return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0;
+ }
+
+ private void throwIfClosedLocked() {
+ if (!mIsOpen) {
+ throw new IllegalStateException("Cannot perform this operation "
+ + "because the connection pool have been closed.");
+ }
+ }
+
+ private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime,
+ int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) {
+ ConnectionWaiter waiter = mConnectionWaiterPool;
+ if (waiter != null) {
+ mConnectionWaiterPool = waiter.mNext;
+ waiter.mNext = null;
+ } else {
+ waiter = new ConnectionWaiter();
+ }
+ waiter.mThread = thread;
+ waiter.mStartTime = startTime;
+ waiter.mPriority = priority;
+ waiter.mWantPrimaryConnection = wantPrimaryConnection;
+ waiter.mSql = sql;
+ waiter.mConnectionFlags = connectionFlags;
+ return waiter;
+ }
+
+ private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) {
+ waiter.mNext = mConnectionWaiterPool;
+ waiter.mThread = null;
+ waiter.mSql = null;
+ waiter.mAssignedConnection = null;
+ waiter.mException = null;
+ waiter.mNonce += 1;
+ mConnectionWaiterPool = waiter;
+ }
+
+ /**
+ * Dumps debugging information about this connection pool.
+ *
+ * @param printer The printer to receive the dump, not null.
+ * @param verbose True to dump more verbose information.
+ */
+ public void dump(Printer printer, boolean verbose) {
+ Printer indentedPrinter = PrefixPrinter.create(printer, " ");
+ synchronized (mLock) {
+ printer.println("Connection pool for " + mConfiguration.path + ":");
+ printer.println(" Open: " + mIsOpen);
+ printer.println(" Max connections: " + mConfiguration.maxConnectionPoolSize);
+
+ printer.println(" Available primary connection:");
+ if (mAvailablePrimaryConnection != null) {
+ mAvailablePrimaryConnection.dump(indentedPrinter, verbose);
+ } else {
+ indentedPrinter.println("<none>");
+ }
+
+ printer.println(" Available non-primary connections:");
+ if (!mAvailableNonPrimaryConnections.isEmpty()) {
+ final int count = mAvailableNonPrimaryConnections.size();
+ for (int i = 0; i < count; i++) {
+ mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose);
+ }
+ } else {
+ indentedPrinter.println("<none>");
+ }
+
+ printer.println(" Acquired connections:");
+ if (!mAcquiredConnections.isEmpty()) {
+ for (Map.Entry<SQLiteConnection, Boolean> entry :
+ mAcquiredConnections.entrySet()) {
+ final SQLiteConnection connection = entry.getKey();
+ connection.dumpUnsafe(indentedPrinter, verbose);
+ indentedPrinter.println(" Pending reconfiguration: " + entry.getValue());
+ }
+ } else {
+ indentedPrinter.println("<none>");
+ }
+
+ printer.println(" Connection waiters:");
+ if (mConnectionWaiterQueue != null) {
+ int i = 0;
+ final long now = SystemClock.uptimeMillis();
+ for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null;
+ waiter = waiter.mNext, i++) {
+ indentedPrinter.println(i + ": waited for "
+ + ((now - waiter.mStartTime) * 0.001f)
+ + " ms - thread=" + waiter.mThread
+ + ", priority=" + waiter.mPriority
+ + ", sql='" + waiter.mSql + "'");
+ }
+ } else {
+ indentedPrinter.println("<none>");
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteConnectionPool: " + mConfiguration.path;
+ }
+
+ private static final class ConnectionWaiter {
+ public ConnectionWaiter mNext;
+ public Thread mThread;
+ public long mStartTime;
+ public int mPriority;
+ public boolean mWantPrimaryConnection;
+ public String mSql;
+ public int mConnectionFlags;
+ public SQLiteConnection mAssignedConnection;
+ public RuntimeException mException;
+ public int mNonce;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java
index c24acd4..82bb23e 100644
--- a/core/java/android/database/sqlite/SQLiteCursor.java
+++ b/core/java/android/database/sqlite/SQLiteCursor.java
@@ -18,6 +18,7 @@ package android.database.sqlite;
import android.database.AbstractWindowedCursor;
import android.database.CursorWindow;
+import android.database.DatabaseUtils;
import android.os.StrictMode;
import android.util.Log;
@@ -42,13 +43,16 @@ public class SQLiteCursor extends AbstractWindowedCursor {
private final String[] mColumns;
/** The query object for the cursor */
- private SQLiteQuery mQuery;
+ private final SQLiteQuery mQuery;
/** The compiled query this cursor came from */
private final SQLiteCursorDriver mDriver;
/** The number of rows in the cursor */
- private volatile int mCount = NO_COUNT;
+ private int mCount = NO_COUNT;
+
+ /** The number of rows that can fit in the cursor window, 0 if unknown */
+ private int mCursorWindowCapacity;
/** A mapping of column names to column indices, to speed up lookups */
private Map<String, Integer> mColumnNameMap;
@@ -61,8 +65,7 @@ public class SQLiteCursor extends AbstractWindowedCursor {
* interface. For a query such as: {@code SELECT name, birth, phone FROM
* myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
* phone) would be in the projection argument and everything from
- * {@code FROM} onward would be in the params argument. This constructor
- * has package scope.
+ * {@code FROM} onward would be in the params argument.
*
* @param db a reference to a Database object that is already constructed
* and opened. This param is not used any longer
@@ -82,8 +85,7 @@ public class SQLiteCursor extends AbstractWindowedCursor {
* interface. For a query such as: {@code SELECT name, birth, phone FROM
* myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
* phone) would be in the projection argument and everything from
- * {@code FROM} onward would be in the params argument. This constructor
- * has package scope.
+ * {@code FROM} onward would be in the params argument.
*
* @param editTable the name of the table used for this query
* @param query the {@link SQLiteQuery} object associated with this cursor object.
@@ -92,9 +94,6 @@ public class SQLiteCursor extends AbstractWindowedCursor {
if (query == null) {
throw new IllegalArgumentException("query object cannot be null");
}
- if (query.mDatabase == null) {
- throw new IllegalArgumentException("query.mDatabase cannot be null");
- }
if (StrictMode.vmSqliteObjectLeaksEnabled()) {
mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
} else {
@@ -105,38 +104,21 @@ public class SQLiteCursor extends AbstractWindowedCursor {
mColumnNameMap = null;
mQuery = query;
- query.mDatabase.lock(query.mSql);
- try {
- // Setup the list of columns
- int columnCount = mQuery.columnCountLocked();
- mColumns = new String[columnCount];
-
- // Read in all column names
- for (int i = 0; i < columnCount; i++) {
- String columnName = mQuery.columnNameLocked(i);
- mColumns[i] = columnName;
- if (false) {
- Log.v("DatabaseWindow", "mColumns[" + i + "] is "
- + mColumns[i]);
- }
-
- // Make note of the row ID column index for quick access to it
- if ("_id".equals(columnName)) {
- mRowIdColumnIndex = i;
- }
+ mColumns = query.getColumnNames();
+ for (int i = 0; i < mColumns.length; i++) {
+ // Make note of the row ID column index for quick access to it
+ if ("_id".equals(mColumns[i])) {
+ mRowIdColumnIndex = i;
}
- } finally {
- query.mDatabase.unlock();
}
}
/**
+ * Get the database that this cursor is associated with.
* @return the SQLiteDatabase that this cursor is associated with.
*/
public SQLiteDatabase getDatabase() {
- synchronized (this) {
- return mQuery.mDatabase;
- }
+ return mQuery.getDatabase();
}
@Override
@@ -158,25 +140,23 @@ public class SQLiteCursor extends AbstractWindowedCursor {
return mCount;
}
- private void fillWindow(int startPos) {
+ private void fillWindow(int requiredPos) {
clearOrCreateWindow(getDatabase().getPath());
- mWindow.setStartPosition(startPos);
- int count = getQuery().fillWindow(mWindow);
- if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0
+
+ if (mCount == NO_COUNT) {
+ int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
+ mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
+ mCursorWindowCapacity = mWindow.getNumRows();
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "received count(*) from native_fill_window: " + count);
+ Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
}
- mCount = count;
- } else if (mCount <= 0) {
- throw new IllegalStateException("Row count should never be zero or negative "
- + "when the start position is non-zero");
+ } else {
+ int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
+ mCursorWindowCapacity);
+ mQuery.fillWindow(mWindow, startPos, requiredPos, false);
}
}
- private synchronized SQLiteQuery getQuery() {
- return mQuery;
- }
-
@Override
public int getColumnIndex(String columnName) {
// Create mColumnNameMap on demand
@@ -231,75 +211,28 @@ public class SQLiteCursor extends AbstractWindowedCursor {
if (isClosed()) {
return false;
}
- long timeStart = 0;
- if (false) {
- timeStart = System.currentTimeMillis();
- }
synchronized (this) {
+ if (!mQuery.getDatabase().isOpen()) {
+ return false;
+ }
+
if (mWindow != null) {
mWindow.clear();
}
mPos = -1;
- SQLiteDatabase db = null;
- try {
- db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- }
- if (!db.equals(mQuery.mDatabase)) {
- // since we need to use a different database connection handle,
- // re-compile the query
- try {
- db.lock(mQuery.mSql);
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- }
- try {
- // close the old mQuery object and open a new one
- mQuery.close();
- mQuery = new SQLiteQuery(db, mQuery);
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- } finally {
- db.unlock();
- }
- }
- // This one will recreate the temp table, and get its count
- mDriver.cursorRequeried(this);
mCount = NO_COUNT;
- try {
- mQuery.requery();
- } catch (IllegalStateException e) {
- // for backwards compatibility, just return false
- Log.w(TAG, "requery() failed " + e.getMessage(), e);
- return false;
- }
- }
- if (false) {
- Log.v("DatabaseWindow", "closing window in requery()");
- Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
+ mDriver.cursorRequeried(this);
}
- boolean result = false;
try {
- result = super.requery();
+ return super.requery();
} catch (IllegalStateException e) {
// for backwards compatibility, just return false
Log.w(TAG, "requery() failed " + e.getMessage(), e);
+ return false;
}
- if (false) {
- long timeEnd = System.currentTimeMillis();
- Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
- }
- return result;
}
@Override
@@ -324,20 +257,16 @@ public class SQLiteCursor extends AbstractWindowedCursor {
// if the cursor hasn't been closed yet, close it first
if (mWindow != null) {
if (mStackTrace != null) {
- int len = mQuery.mSql.length();
+ String sql = mQuery.getSql();
+ int len = sql.length();
StrictMode.onSqliteObjectLeaked(
"Finalizing a Cursor that has not been deactivated or closed. " +
- "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
- ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len),
+ "database = " + mQuery.getDatabase().getLabel() +
+ ", table = " + mEditTable +
+ ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
mStackTrace);
}
close();
- SQLiteDebug.notifyActiveCursorFinalized();
- } else {
- if (false) {
- Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
- ", table = " + mEditTable + ", query = " + mQuery.mSql);
- }
}
} finally {
super.finalize();
diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java
index b3963f9..ad2cdd2 100644
--- a/core/java/android/database/sqlite/SQLiteCursorDriver.java
+++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java
@@ -39,7 +39,7 @@ public interface SQLiteCursorDriver {
void cursorDeactivated();
/**
- * Called by a SQLiteCursor when it is requeryed.
+ * Called by a SQLiteCursor when it is requeried.
*/
void cursorRequeried(Cursor cursor);
diff --git a/core/java/android/database/sqlite/SQLiteCustomFunction.java b/core/java/android/database/sqlite/SQLiteCustomFunction.java
new file mode 100644
index 0000000..02f3284
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCustomFunction.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * Describes a custom SQL function.
+ *
+ * @hide
+ */
+public final class SQLiteCustomFunction {
+ public final String name;
+ public final int numArgs;
+ public final SQLiteDatabase.CustomFunction callback;
+
+ /**
+ * Create custom function.
+ *
+ * @param name The name of the sqlite3 function.
+ * @param numArgs The number of arguments for the function, or -1 to
+ * support any number of arguments.
+ * @param callback The callback to invoke when the function is executed.
+ */
+ public SQLiteCustomFunction(String name, int numArgs,
+ SQLiteDatabase.CustomFunction callback) {
+ if (name == null) {
+ throw new IllegalArgumentException("name must not be null.");
+ }
+
+ this.name = name;
+ this.numArgs = numArgs;
+ this.callback = callback;
+ }
+
+ // Called from native.
+ @SuppressWarnings("unused")
+ private void dispatchCallback(String[] args) {
+ callback.callback(args);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index f990be6..7db7bfb 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -16,8 +16,9 @@
package android.database.sqlite;
-import android.app.AppGlobals;
+import android.content.CancelationSignal;
import android.content.ContentValues;
+import android.content.OperationCanceledException;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseErrorHandler;
@@ -25,61 +26,117 @@ 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.os.Looper;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
-import android.util.LruCache;
import android.util.Pair;
-import dalvik.system.BlockGuard;
+import android.util.Printer;
+
+import dalvik.system.CloseGuard;
+
import java.io.File;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Random;
import java.util.WeakHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.regex.Pattern;
/**
* Exposes methods to manage a SQLite database.
- * <p>SQLiteDatabase has methods to create, delete, execute SQL commands, and
+ *
+ * <p>
+ * SQLiteDatabase has methods to create, delete, execute SQL commands, and
* perform other common database management tasks.
- * <p>See the Notepad sample application in the SDK for an example of creating
+ * </p><p>
+ * See the Notepad sample application in the SDK for an example of creating
* and managing a database.
- * <p> Database names must be unique within an application, not across all
- * applications.
+ * </p><p>
+ * Database names must be unique within an application, not across all applications.
+ * </p>
*
* <h3>Localized Collation - ORDER BY</h3>
- * <p>In addition to SQLite's default <code>BINARY</code> collator, Android supplies
- * two more, <code>LOCALIZED</code>, which changes with the system's current locale
- * if you wire it up correctly (XXX a link needed!), and <code>UNICODE</code>, which
- * is the Unicode Collation Algorithm and not tailored to the current locale.
+ * <p>
+ * In addition to SQLite's default <code>BINARY</code> collator, Android supplies
+ * two more, <code>LOCALIZED</code>, which changes with the system's current locale,
+ * and <code>UNICODE</code>, which is the Unicode Collation Algorithm and not tailored
+ * to the current locale.
+ * </p>
*/
public class SQLiteDatabase extends SQLiteClosable {
private static final String TAG = "SQLiteDatabase";
- private static final boolean ENABLE_DB_SAMPLE = false; // true to enable stats in event log
- private static final int EVENT_DB_OPERATION = 52000;
+
private static final int EVENT_DB_CORRUPT = 75004;
- /**
- * Algorithms used in ON CONFLICT clause
- * http://www.sqlite.org/lang_conflict.html
- */
- /**
- * When a constraint violation occurs, an immediate ROLLBACK occurs,
+ // Stores reference to all databases opened in the current process.
+ // (The referent Object is not used at this time.)
+ // INVARIANT: Guarded by sActiveDatabases.
+ private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases =
+ new WeakHashMap<SQLiteDatabase, Object>();
+
+ // Thread-local for database sessions that belong to this database.
+ // Each thread has its own database session.
+ // INVARIANT: Immutable.
+ private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() {
+ @Override
+ protected SQLiteSession initialValue() {
+ return createSession();
+ }
+ };
+
+ // The optional factory to use when creating new Cursors. May be null.
+ // INVARIANT: Immutable.
+ private final CursorFactory mCursorFactory;
+
+ // Error handler to be used when SQLite returns corruption errors.
+ // INVARIANT: Immutable.
+ private final DatabaseErrorHandler mErrorHandler;
+
+ // Shared database state lock.
+ // This lock guards all of the shared state of the database, such as its
+ // configuration, whether it is open or closed, and so on. This lock should
+ // be held for as little time as possible.
+ //
+ // The lock MUST NOT be held while attempting to acquire database connections or
+ // while executing SQL statements on behalf of the client as it can lead to deadlock.
+ //
+ // It is ok to hold the lock while reconfiguring the connection pool or dumping
+ // statistics because those operations are non-reentrant and do not try to acquire
+ // connections that might be held by other threads.
+ //
+ // Basic rule: grab the lock, access or modify global state, release the lock, then
+ // do the required SQL work.
+ private final Object mLock = new Object();
+
+ // Warns if the database is finalized without being closed properly.
+ // INVARIANT: Guarded by mLock.
+ private final CloseGuard mCloseGuardLocked = CloseGuard.get();
+
+ // The database configuration.
+ // INVARIANT: Guarded by mLock.
+ private final SQLiteDatabaseConfiguration mConfigurationLocked;
+
+ // The connection pool for the database, null when closed.
+ // The pool itself is thread-safe, but the reference to it can only be acquired
+ // when the lock is held.
+ // INVARIANT: Guarded by mLock.
+ private SQLiteConnectionPool mConnectionPoolLocked;
+
+ // True if the database has attached databases.
+ // INVARIANT: Guarded by mLock.
+ private boolean mHasAttachedDbsLocked;
+
+ // True if the database is in WAL mode.
+ // INVARIANT: Guarded by mLock.
+ private boolean mIsWALEnabledLocked;
+
+ /**
+ * When a constraint violation occurs, an immediate ROLLBACK occurs,
* thus ending the current transaction, and the command aborts with a
* return code of SQLITE_CONSTRAINT. If no transaction is active
* (other than the implied transaction that is created on every command)
- * then this algorithm works the same as ABORT.
+ * then this algorithm works the same as ABORT.
*/
public static final int CONFLICT_ROLLBACK = 1;
@@ -118,14 +175,15 @@ public class SQLiteDatabase extends SQLiteClosable {
* violation occurs then the IGNORE algorithm is used. When this conflict
* resolution strategy deletes rows in order to satisfy a constraint,
* it does not invoke delete triggers on those rows.
- * This behavior might change in a future release.
+ * This behavior might change in a future release.
*/
public static final int CONFLICT_REPLACE = 5;
/**
- * use the following when no conflict action is specified.
+ * Use the following when no conflict action is specified.
*/
public static final int CONFLICT_NONE = 0;
+
private static final String[] CONFLICT_VALUES = new String[]
{"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
@@ -146,7 +204,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000;
/**
- * Flag for {@link #openDatabase} to open the database for reading and writing.
+ * Open flag: Flag for {@link #openDatabase} to open the database for reading and writing.
* If the disk is full, this may fail even before you actually write anything.
*
* {@more} Note that the value of this flag is 0, so it is the default.
@@ -154,7 +212,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public static final int OPEN_READWRITE = 0x00000000; // update native code if changing
/**
- * Flag for {@link #openDatabase} to open the database for reading only.
+ * Open flag: Flag for {@link #openDatabase} to open the database for reading only.
* This is the only reliable way to open a database if the disk may be full.
*/
public static final int OPEN_READONLY = 0x00000001; // update native code if changing
@@ -162,7 +220,8 @@ public class SQLiteDatabase extends SQLiteClosable {
private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing
/**
- * Flag for {@link #openDatabase} to open the database without support for localized collators.
+ * Open flag: Flag for {@link #openDatabase} to open the database without support for
+ * localized collators.
*
* {@more} This causes the collator <code>LOCALIZED</code> not to be created.
* You must be consistent when using this flag to use the setting the database was
@@ -171,190 +230,62 @@ public class SQLiteDatabase extends SQLiteClosable {
public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing
/**
- * Flag for {@link #openDatabase} to create the database file if it does not already exist.
+ * Open flag: Flag for {@link #openDatabase} to create the database file if it does not
+ * already exist.
*/
public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing
/**
- * Indicates whether the most-recently started transaction has been marked as successful.
- */
- private boolean mInnerTransactionIsSuccessful;
-
- /**
- * Valid during the life of a transaction, and indicates whether the entire transaction (the
- * outer one and all of the inner ones) so far has been successful.
- */
- private boolean mTransactionIsSuccessful;
-
- /**
- * Valid during the life of a transaction.
- */
- private SQLiteTransactionListener mTransactionListener;
-
- /**
- * this member is set if {@link #execSQL(String)} is used to begin and end transactions.
- */
- private boolean mTransactionUsingExecSql;
-
- /** Synchronize on this when accessing the database */
- private final DatabaseReentrantLock mLock = new DatabaseReentrantLock(true);
-
- private long mLockAcquiredWallTime = 0L;
- private long mLockAcquiredThreadTime = 0L;
-
- // limit the frequency of complaints about each database to one within 20 sec
- // unless run command adb shell setprop log.tag.Database VERBOSE
- private static final int LOCK_WARNING_WINDOW_IN_MS = 20000;
- /** If the lock is held this long then a warning will be printed when it is released. */
- private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300;
- private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100;
- private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000;
-
- private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000;
-
- // The pattern we remove from database filenames before
- // potentially logging them.
- private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
-
- private long mLastLockMessageTime = 0L;
-
- // Things related to query logging/sampling for debugging
- // slow/frequent queries during development. Always log queries
- // which take (by default) 500ms+; shorter queries are sampled
- // accordingly. Commit statements, which are typically slow, are
- // logged together with the most recently executed SQL statement,
- // for disambiguation. The 500ms value is configurable via a
- // SystemProperty, but developers actively debugging database I/O
- // should probably use the regular log tunable,
- // LOG_SLOW_QUERIES_PROPERTY, defined below.
- private static int sQueryLogTimeInMillis = 0; // lazily initialized
- private static final int QUERY_LOG_SQL_LENGTH = 64;
- private static final String COMMIT_SQL = "COMMIT;";
- private static final String BEGIN_SQL = "BEGIN;";
- private final Random mRandom = new Random();
- /** the last non-commit/rollback sql statement in a transaction */
- // guarded by 'this'
- private String mLastSqlStatement = null;
-
- synchronized String getLastSqlStatement() {
- return mLastSqlStatement;
- }
-
- synchronized void setLastSqlStatement(String sql) {
- mLastSqlStatement = sql;
- }
-
- /** guarded by {@link #mLock} */
- private long mTransStartTime;
-
- // String prefix for slow database query EventLog records that show
- // lock acquistions of the database.
- /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:";
-
- /** Used by native code, do not rename. make it volatile, so it is thread-safe. */
- /* package */ volatile int mNativeHandle = 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 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 final int mFlags;
-
- /** The optional factory to use when creating new Cursors */
- private final CursorFactory mFactory;
-
- private final WeakHashMap<SQLiteClosable, Object> mPrograms;
-
- /** Default statement-cache size per database connection ( = instance of this class) */
- private static final int DEFAULT_SQL_CACHE_SIZE = 25;
-
- /**
- * 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
- * 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.
- *
- * why is this cache NOT static? because sqlite attaches compiledsql statements to the
- * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is
- * invoked.
+ * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}.
*
- * this cache's max size is settable by calling the method
- * (@link #setMaxSqlCacheSize(int)}.
- */
- // guarded by this
- private LruCache<String, SQLiteCompiledSql> mCompiledQueries;
-
- /**
- * 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.
+ * Each prepared-statement is between 1K - 6K, depending on the complexity of the
+ * SQL statement & schema. A large SQL cache may use a significant amount of memory.
*/
public static final int MAX_SQL_CACHE_SIZE = 100;
- private boolean mCacheFullWarning;
-
- /** Used to find out where this object was created in case it never got closed. */
- private final Throwable mStackTrace;
-
- /** 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.
- */
- /* 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;
- /** on pooled database connections, this member points to the parent ( = main)
- * database connection handle.
- * package visibility only for testing purposes
- */
- /* package */ SQLiteDatabase mParentConnObj = null;
-
- private static final String MEMORY_DB_PATH = ":memory:";
-
- /** set to true if the database has attached databases */
- private volatile boolean mHasAttachedDbs = false;
-
- /** stores reference to all databases opened in the current process. */
- private static ArrayList<WeakReference<SQLiteDatabase>> mActiveDatabases =
- new ArrayList<WeakReference<SQLiteDatabase>>();
-
- 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);
+ private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory,
+ DatabaseErrorHandler errorHandler) {
+ mCursorFactory = cursorFactory;
+ mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
+ mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
}
- synchronized void removeSQLiteClosable(SQLiteClosable closable) {
- mPrograms.remove(closable);
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose(true);
+ } finally {
+ super.finalize();
+ }
}
@Override
protected void onAllReferencesReleased() {
- if (isOpen()) {
- // close the database which will close all pending statements to be finalized also
- close();
+ dispose(false);
+ }
+
+ private void dispose(boolean finalized) {
+ final SQLiteConnectionPool pool;
+ synchronized (mLock) {
+ if (mCloseGuardLocked != null) {
+ if (finalized) {
+ mCloseGuardLocked.warnIfOpen();
+ }
+ mCloseGuardLocked.close();
+ }
+
+ pool = mConnectionPoolLocked;
+ mConnectionPoolLocked = null;
+ }
+
+ if (!finalized) {
+ synchronized (sActiveDatabases) {
+ sActiveDatabases.remove(this);
+ }
+
+ if (pool != null) {
+ pool.close();
+ }
}
}
@@ -364,7 +295,9 @@ public class SQLiteDatabase extends SQLiteClosable {
*
* @return the number of bytes actually released
*/
- static public native int releaseMemory();
+ public static int releaseMemory() {
+ return SQLiteGlobal.releaseMemory();
+ }
/**
* Control whether or not the SQLiteDatabase is made thread-safe by using locks
@@ -372,159 +305,82 @@ public class SQLiteDatabase extends SQLiteClosable {
* DB will only be used by a single thread then you should set this to false.
* The default is true.
* @param lockingEnabled set to true to enable locks, false otherwise
+ *
+ * @deprecated This method now does nothing. Do not use.
*/
+ @Deprecated
public void setLockingEnabled(boolean lockingEnabled) {
- mLockingEnabled = lockingEnabled;
}
/**
- * If set then the SQLiteDatabase is made thread-safe by using locks
- * around critical sections
+ * Gets a label to use when describing the database in log messages.
+ * @return The label.
*/
- private boolean mLockingEnabled = true;
-
- /* package */ void onCorruption() {
- EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
- mErrorHandler.onCorruption(this);
+ String getLabel() {
+ synchronized (mLock) {
+ return mConfigurationLocked.label;
+ }
}
/**
- * Locks the database for exclusive access. The database lock must be held when
- * touch the native sqlite3* object since it is single threaded and uses
- * a polling lock contention algorithm. The lock is recursive, and may be acquired
- * multiple times by the same thread. This is a no-op if mLockingEnabled is false.
- *
- * @see #unlock()
+ * Sends a corruption message to the database error handler.
*/
- /* package */ void lock(String sql) {
- lock(sql, false);
- }
-
- /* pachage */ void lock() {
- lock(null, false);
- }
-
- private static final long LOCK_WAIT_PERIOD = 30L;
- private void lock(String sql, boolean forced) {
- // make sure this method is NOT being called from a 'synchronized' method
- if (Thread.holdsLock(this)) {
- Log.w(TAG, "don't lock() while in a synchronized method");
- }
- verifyDbIsOpen();
- if (!forced && !mLockingEnabled) return;
- boolean done = false;
- long timeStart = SystemClock.uptimeMillis();
- while (!done) {
- try {
- // wait for 30sec to acquire the lock
- done = mLock.tryLock(LOCK_WAIT_PERIOD, TimeUnit.SECONDS);
- if (!done) {
- // lock not acquired in NSec. print a message and stacktrace saying the lock
- // has not been available for 30sec.
- Log.w(TAG, "database lock has not been available for " + LOCK_WAIT_PERIOD +
- " sec. Current Owner of the lock is " + mLock.getOwnerDescription() +
- ". Continuing to wait in thread: " + Thread.currentThread().getId());
- }
- } catch (InterruptedException e) {
- // ignore the interruption
- }
- }
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- // Use elapsed real-time since the CPU may sleep when waiting for IO
- mLockAcquiredWallTime = SystemClock.elapsedRealtime();
- mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
- }
- }
- if (sql != null) {
- if (ENABLE_DB_SAMPLE) {
- logTimeStat(sql, timeStart, GET_LOCK_LOG_PREFIX);
- }
- }
- }
- private static class DatabaseReentrantLock extends ReentrantLock {
- DatabaseReentrantLock(boolean fair) {
- super(fair);
- }
- @Override
- public Thread getOwner() {
- return super.getOwner();
- }
- public String getOwnerDescription() {
- Thread t = getOwner();
- return (t== null) ? "none" : String.valueOf(t.getId());
- }
+ void onCorruption() {
+ EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel());
+ mErrorHandler.onCorruption(this);
}
/**
- * Locks the database for exclusive access. The database lock must be held when
- * touch the native sqlite3* object since it is single threaded and uses
- * a polling lock contention algorithm. The lock is recursive, and may be acquired
- * multiple times by the same thread.
+ * Gets the {@link SQLiteSession} that belongs to this thread for this database.
+ * Once a thread has obtained a session, it will continue to obtain the same
+ * session even after the database has been closed (although the session will not
+ * be usable). However, a thread that does not already have a session cannot
+ * obtain one after the database has been closed.
+ *
+ * The idea is that threads that have active connections to the database may still
+ * have work to complete even after the call to {@link #close}. Active database
+ * connections are not actually disposed until they are released by the threads
+ * that own them.
*
- * @see #unlockForced()
+ * @return The session, never null.
+ *
+ * @throws IllegalStateException if the thread does not yet have a session and
+ * the database is not open.
*/
- private void lockForced() {
- lock(null, true);
+ SQLiteSession getThreadSession() {
+ return mThreadSession.get(); // initialValue() throws if database closed
}
- private void lockForced(String sql) {
- lock(sql, true);
- }
-
- /**
- * Releases the database lock. This is a no-op if mLockingEnabled is false.
- *
- * @see #unlock()
- */
- /* package */ void unlock() {
- if (!mLockingEnabled) return;
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- checkLockHoldTime();
- }
+ SQLiteSession createSession() {
+ final SQLiteConnectionPool pool;
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ pool = mConnectionPoolLocked;
}
- mLock.unlock();
+ return new SQLiteSession(pool);
}
/**
- * Releases the database lock.
+ * Gets default connection flags that are appropriate for this thread, taking into
+ * account whether the thread is acting on behalf of the UI.
*
- * @see #unlockForced()
+ * @param readOnly True if the connection should be read-only.
+ * @return The connection flags.
*/
- private void unlockForced() {
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- checkLockHoldTime();
- }
+ int getThreadDefaultConnectionFlags(boolean readOnly) {
+ int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY :
+ SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY;
+ if (isMainThread()) {
+ flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE;
}
- mLock.unlock();
+ return flags;
}
- private void checkLockHoldTime() {
- // Use elapsed real-time since the CPU may sleep when waiting for IO
- long elapsedTime = SystemClock.elapsedRealtime();
- long lockedTime = elapsedTime - mLockAcquiredWallTime;
- if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT &&
- !Log.isLoggable(TAG, Log.VERBOSE) &&
- (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) {
- return;
- }
- if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) {
- int threadTime = (int)
- ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000);
- if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS ||
- lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) {
- mLastLockMessageTime = elapsedTime;
- String msg = "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was "
- + threadTime + "ms";
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) {
- Log.d(TAG, msg, new Exception());
- } else {
- Log.d(TAG, msg);
- }
- }
- }
+ private static boolean isMainThread() {
+ // FIXME: There should be a better way to do this.
+ // Would also be nice to have something that would work across Binder calls.
+ Looper looper = Looper.myLooper();
+ return looper != null && looper == Looper.getMainLooper();
}
/**
@@ -636,50 +492,9 @@ public class SQLiteDatabase extends SQLiteClosable {
private void beginTransaction(SQLiteTransactionListener transactionListener,
boolean exclusive) {
- verifyDbIsOpen();
- lockForced(BEGIN_SQL);
- boolean ok = false;
- try {
- // If this thread already had the lock then get out
- if (mLock.getHoldCount() > 1) {
- if (mInnerTransactionIsSuccessful) {
- String msg = "Cannot call beginTransaction between "
- + "calling setTransactionSuccessful and endTransaction";
- IllegalStateException e = new IllegalStateException(msg);
- Log.e(TAG, "beginTransaction() failed", e);
- throw e;
- }
- ok = true;
- return;
- }
-
- // This thread didn't already have the lock, so begin a database
- // transaction now.
- if (exclusive && mConnectionPool == null) {
- execSQL("BEGIN EXCLUSIVE;");
- } else {
- execSQL("BEGIN IMMEDIATE;");
- }
- mTransStartTime = SystemClock.uptimeMillis();
- mTransactionListener = transactionListener;
- mTransactionIsSuccessful = true;
- mInnerTransactionIsSuccessful = false;
- if (transactionListener != null) {
- try {
- transactionListener.onBegin();
- } catch (RuntimeException e) {
- execSQL("ROLLBACK;");
- throw e;
- }
- }
- ok = true;
- } finally {
- if (!ok) {
- // beginTransaction is called before the try block so we must release the lock in
- // the case of failure.
- unlockForced();
- }
- }
+ getThreadSession().beginTransaction(exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
+ SQLiteSession.TRANSACTION_MODE_IMMEDIATE, transactionListener,
+ getThreadDefaultConnectionFlags(false /*readOnly*/), null);
}
/**
@@ -687,68 +502,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* are committed and rolled back.
*/
public void endTransaction() {
- verifyLockOwner();
- try {
- if (mInnerTransactionIsSuccessful) {
- mInnerTransactionIsSuccessful = false;
- } else {
- mTransactionIsSuccessful = false;
- }
- if (mLock.getHoldCount() != 1) {
- return;
- }
- RuntimeException savedException = null;
- if (mTransactionListener != null) {
- try {
- if (mTransactionIsSuccessful) {
- mTransactionListener.onCommit();
- } else {
- mTransactionListener.onRollback();
- }
- } catch (RuntimeException e) {
- savedException = e;
- mTransactionIsSuccessful = false;
- }
- }
- 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");
- }
- }
- // log the transaction time to the Eventlog.
- if (ENABLE_DB_SAMPLE) {
- logTimeStat(getLastSqlStatement(), mTransStartTime, COMMIT_SQL);
- }
- } else {
- try {
- execSQL("ROLLBACK;");
- if (savedException != null) {
- throw savedException;
- }
- } catch (SQLException e) {
- if (false) {
- Log.d(TAG, "exception during rollback, maybe the DB previously "
- + "performed an auto-rollback");
- }
- }
- }
- } finally {
- mTransactionListener = null;
- unlockForced();
- if (false) {
- Log.v(TAG, "unlocked " + Thread.currentThread()
- + ", holdCount is " + mLock.getHoldCount());
- }
- }
+ getThreadSession().endTransaction(null);
}
/**
@@ -761,86 +515,46 @@ public class SQLiteDatabase extends SQLiteClosable {
* transaction is already marked as successful.
*/
public void setTransactionSuccessful() {
- verifyDbIsOpen();
- if (!mLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("no transaction pending");
- }
- if (mInnerTransactionIsSuccessful) {
- throw new IllegalStateException(
- "setTransactionSuccessful may only be called once per call to beginTransaction");
- }
- mInnerTransactionIsSuccessful = true;
+ getThreadSession().setTransactionSuccessful();
}
/**
- * return true if there is a transaction pending
+ * Returns true if the current thread has a transaction pending.
+ *
+ * @return True if the current thread is in a transaction.
*/
public boolean inTransaction() {
- return mLock.getHoldCount() > 0 || mTransactionUsingExecSql;
- }
-
- /* package */ synchronized void setTransactionUsingExecSqlFlag() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "found execSQL('begin transaction')");
- }
- mTransactionUsingExecSql = true;
- }
-
- /* package */ synchronized void resetTransactionUsingExecSqlFlag() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- if (mTransactionUsingExecSql) {
- Log.i(TAG, "found execSQL('commit or end or rollback')");
- }
- }
- mTransactionUsingExecSql = false;
+ return getThreadSession().hasTransaction();
}
/**
- * Returns true if the caller is considered part of the current transaction, if any.
+ * Returns true if the current thread is holding an active connection to the database.
* <p>
- * Caller is part of the current transaction if either of the following is true
- * <ol>
- * <li>If transaction is started by calling beginTransaction() methods AND if the caller is
- * in the same thread as the thread that started the transaction.
- * </li>
- * <li>If the transaction is started by calling {@link #execSQL(String)} like this:
- * execSQL("BEGIN transaction"). In this case, every thread in the process is considered
- * part of the current transaction.</li>
- * </ol>
- *
- * @return true if the caller is considered part of the current transaction, if any.
- */
- /* package */ synchronized boolean amIInTransaction() {
- // always do this test on the main database connection - NOT on pooled database connection
- // since transactions always occur on the main database connections only.
- SQLiteDatabase db = (isPooledConnection()) ? mParentConnObj : this;
- boolean b = (!db.inTransaction()) ? false :
- db.mTransactionUsingExecSql || db.mLock.isHeldByCurrentThread();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "amIinTransaction: " + b);
- }
- return b;
- }
-
- /**
- * Checks if the database lock is held by this thread.
+ * The name of this method comes from a time when having an active connection
+ * to the database meant that the thread was holding an actual lock on the
+ * database. Nowadays, there is no longer a true "database lock" although threads
+ * may block if they cannot acquire a database connection to perform a
+ * particular operation.
+ * </p>
*
- * @return true, if this thread is holding the database lock.
+ * @return True if the current thread is holding an active connection to the database.
*/
public boolean isDbLockedByCurrentThread() {
- return mLock.isHeldByCurrentThread();
+ return getThreadSession().hasConnection();
}
/**
- * Checks if the database is locked by another thread. This is
- * just an estimate, since this status can change at any time,
- * including after the call is made but before the result has
- * been acted upon.
+ * Always returns false.
+ * <p>
+ * There is no longer the concept of a database lock, so this method always returns false.
+ * </p>
*
- * @return true, if the database is locked by another thread
+ * @return False.
+ * @deprecated Always returns false. Do not use this method.
*/
+ @Deprecated
public boolean isDbLockedByOtherThreads() {
- return !mLock.isHeldByCurrentThread() && mLock.isLocked();
+ return false;
}
/**
@@ -884,46 +598,12 @@ public class SQLiteDatabase extends SQLiteClosable {
return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay);
}
- private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) {
- if (mLock.getQueueLength() == 0) {
- // Reset the lock acquire time since we know that the thread was willing to yield
- // the lock at this time.
- mLockAcquiredWallTime = SystemClock.elapsedRealtime();
- mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
- return false;
- }
- setTransactionSuccessful();
- SQLiteTransactionListener transactionListener = mTransactionListener;
- endTransaction();
- if (checkFullyYielded) {
- if (this.isDbLockedByCurrentThread()) {
- throw new IllegalStateException(
- "Db locked more than once. yielfIfContended cannot yield");
- }
- }
- if (sleepAfterYieldDelay > 0) {
- // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to
- // check if anyone is using the database. If the database is not contended,
- // retake the lock and return.
- long remainingDelay = sleepAfterYieldDelay;
- while (remainingDelay > 0) {
- try {
- Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ?
- remainingDelay : SLEEP_AFTER_YIELD_QUANTUM);
- } catch (InterruptedException e) {
- Thread.interrupted();
- }
- remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM;
- if (mLock.getQueueLength() == 0) {
- break;
- }
- }
- }
- beginTransactionWithListener(transactionListener);
- return true;
+ private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) {
+ return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe, null);
}
/**
+ * Deprecated.
* @deprecated This method no longer serves any useful purpose and has been deprecated.
*/
@Deprecated
@@ -932,19 +612,6 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Used to allow returning sub-classes of {@link Cursor} when calling query.
- */
- public interface CursorFactory {
- /**
- * See
- * {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
- */
- public Cursor newCursor(SQLiteDatabase db,
- SQLiteCursorDriver masterQuery, String editTable,
- SQLiteQuery query);
- }
-
- /**
* Open the database according to the flags {@link #OPEN_READWRITE}
* {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
*
@@ -983,50 +650,9 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
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);
- sqliteDatabase.setJournalMode(path, "TRUNCATE");
-
- // add this database to the list of databases opened in this process
- synchronized(mActiveDatabases) {
- mActiveDatabases.add(new WeakReference<SQLiteDatabase>(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 {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "opening the db : " + path);
- }
- // Open the database.
- db.dbopen(path, flags);
- db.setLocale(Locale.getDefault());
- if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
- db.enableSqlTracing(path, connectionNum);
- }
- if (SQLiteDebug.DEBUG_SQL_TIME) {
- db.enableSqlProfiling(path, connectionNum);
- }
- return db;
- } catch (SQLiteDatabaseCorruptException e) {
- 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;
- }
+ SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
+ db.open();
+ return db;
}
/**
@@ -1051,16 +677,46 @@ public class SQLiteDatabase extends SQLiteClosable {
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
+ private void open() {
+ try {
+ try {
+ openInner();
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ openInner();
+ }
+
+ // Disable WAL if it was previously enabled.
+ setJournalMode("TRUNCATE");
+ } catch (SQLiteException ex) {
+ Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
+ close();
+ throw ex;
+ }
+ }
+
+ private void openInner() {
+ synchronized (mLock) {
+ assert mConnectionPoolLocked == null;
+ mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
+ mCloseGuardLocked.open("close");
+ }
+
+ synchronized (sActiveDatabases) {
+ sActiveDatabases.put(this, null);
+ }
+ }
+
+ private void setJournalMode(String mode) {
+ // Journal mode can be set only for non-memory databases
// AND can't be set for readonly databases
- if (dbPath.equalsIgnoreCase(MEMORY_DB_PATH) || isReadOnly()) {
+ if (isInMemoryDatabase() || isReadOnly()) {
return;
}
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);
+ Log.e(TAG, "setting journal_mode to " + mode + " failed for db: " + getLabel()
+ + " (on pragma set journal_mode, sqlite returned:" + s);
}
}
@@ -1077,159 +733,37 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public static SQLiteDatabase create(CursorFactory factory) {
// This is a magic string with special meaning for SQLite.
- return openDatabase(MEMORY_DB_PATH, factory, CREATE_IF_NECESSARY);
+ return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH,
+ factory, CREATE_IF_NECESSARY);
}
/**
* Close the database.
*/
public void close() {
- if (!isOpen()) {
- return;
- }
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.i(TAG, "closing db: " + mPath + " (connection # " + mConnectionNum);
- }
- lock();
- try {
- // some other thread could have closed this database while I was waiting for lock.
- // check the database state
- if (!isOpen()) {
- return;
- }
- closeClosable();
- // finalize ALL statements queued up so far
- closePendingStatements();
- releaseCustomFunctions();
- // close this database instance - regardless of its reference count value
- closeDatabase();
- if (mConnectionPool != null) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert mConnectionPool != null;
- Log.i(TAG, mConnectionPool.toString());
- }
- mConnectionPool.close();
- }
- } finally {
- unlock();
- }
- }
-
- private void closeClosable() {
- /* 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
- * to be closed. sqlite doesn't let a database close if there are
- * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries.
- */
- deallocCachedSqlStatements();
-
- Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
- while (iter.hasNext()) {
- Map.Entry<SQLiteClosable, Object> entry = iter.next();
- SQLiteClosable program = entry.getKey();
- if (program != null) {
- program.onAllReferencesReleasedFromContainer();
- }
- }
- }
-
- /**
- * package level access for testing purposes
- */
- /* package */ void closeDatabase() throws SQLiteException {
- try {
- dbclose();
- } catch (SQLiteUnfinalizedObjectsException e) {
- String msg = e.getMessage();
- String[] tokens = msg.split(",", 2);
- int stmtId = Integer.parseInt(tokens[0]);
- // get extra info about this statement, if it is still to be released by closeClosable()
- Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
- boolean found = false;
- while (iter.hasNext()) {
- Map.Entry<SQLiteClosable, Object> entry = iter.next();
- SQLiteClosable program = entry.getKey();
- if (program != null && program instanceof SQLiteProgram) {
- SQLiteCompiledSql compiledSql = ((SQLiteProgram)program).mCompiledSql;
- if (compiledSql.nStatement == stmtId) {
- msg = compiledSql.toString();
- found = true;
- }
- }
- }
- if (!found) {
- // the statement is already released by closeClosable(). is it waiting to be
- // finalized?
- if (mClosedStatementIds.contains(stmtId)) {
- Log.w(TAG, "this shouldn't happen. finalizing the statement now: ");
- closePendingStatements();
- // try to close the database again
- closeDatabase();
- }
- } else {
- // the statement is not yet closed. most probably programming error in the app.
- throw new SQLiteUnfinalizedObjectsException(
- "close() on database: " + getPath() +
- " failed due to un-close()d SQL statements: " + msg);
- }
- }
- }
-
- /**
- * Native call to close the database.
- */
- private native void dbclose();
-
- /**
- * A callback interface for a custom sqlite3 function.
- * This can be used to create a function that can be called from
- * sqlite3 database triggers.
- * @hide
- */
- public interface CustomFunction {
- public void callback(String[] args);
+ dispose(false);
}
/**
* Registers a CustomFunction callback as a function that can be called from
- * sqlite3 database triggers.
+ * SQLite database triggers.
+ *
* @param name the name of the sqlite3 function
* @param numArgs the number of arguments for the function
* @param function callback to call when the function is executed
* @hide
*/
public void addCustomFunction(String name, int numArgs, CustomFunction function) {
- verifyDbIsOpen();
- synchronized (mCustomFunctions) {
- int ref = native_addCustomFunction(name, numArgs, function);
- if (ref != 0) {
- // save a reference to the function for cleanup later
- mCustomFunctions.add(new Integer(ref));
- } else {
- throw new SQLiteException("failed to add custom function " + name);
- }
- }
- }
+ // Create wrapper (also validates arguments).
+ SQLiteCustomFunction wrapper = new SQLiteCustomFunction(name, numArgs, function);
- private void releaseCustomFunctions() {
- synchronized (mCustomFunctions) {
- for (int i = 0; i < mCustomFunctions.size(); i++) {
- Integer function = mCustomFunctions.get(i);
- native_releaseCustomFunction(function.intValue());
- }
- mCustomFunctions.clear();
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.customFunctions.add(wrapper);
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
}
- // list of CustomFunction references so we can clean up when the database closes
- private final ArrayList<Integer> mCustomFunctions =
- new ArrayList<Integer>();
-
- private native int native_addCustomFunction(String name, int numArgs, CustomFunction function);
- private native void native_releaseCustomFunction(int function);
-
/**
* Gets the database version.
*
@@ -1364,7 +898,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* {@link SQLiteStatement}s are not synchronized, see the documentation for more details.
*/
public SQLiteStatement compileStatement(String sql) throws SQLException {
- verifyDbIsOpen();
+ throwIfNotOpen(); // fail fast
return new SQLiteStatement(this, sql, null);
}
@@ -1403,7 +937,48 @@ public class SQLiteDatabase extends SQLiteClosable {
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit) {
return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
- groupBy, having, orderBy, limit);
+ groupBy, having, orderBy, limit, null);
+ }
+
+ /**
+ * Query the given URL, returning a {@link Cursor} over the result set.
+ *
+ * @param distinct true if you want each row to be unique, false otherwise.
+ * @param table The table name to compile the query against.
+ * @param columns A list of which columns to return. Passing null will
+ * return all columns, which is discouraged to prevent reading
+ * data from storage that isn't going to be used.
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null
+ * will return all rows for the given table.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @param groupBy A filter declaring how to group rows, formatted as an SQL
+ * GROUP BY clause (excluding the GROUP BY itself). Passing null
+ * will cause the rows to not be grouped.
+ * @param having A filter declare which row groups to include in the cursor,
+ * if row grouping is being used, formatted as an SQL HAVING
+ * clause (excluding the HAVING itself). Passing null will cause
+ * all row groups to be included, and is required when row
+ * grouping is not being used.
+ * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+ * (excluding the ORDER BY itself). Passing null will use the
+ * default sort order, which may be unordered.
+ * @param limit Limits the number of rows returned by the query,
+ * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ * @see Cursor
+ */
+ public Cursor query(boolean distinct, String table, String[] columns,
+ String selection, String[] selectionArgs, String groupBy,
+ String having, String orderBy, String limit, CancelationSignal cancelationSignal) {
+ return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
+ groupBy, having, orderBy, limit, cancelationSignal);
}
/**
@@ -1442,12 +1017,55 @@ public class SQLiteDatabase extends SQLiteClosable {
boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit) {
- verifyDbIsOpen();
+ return queryWithFactory(cursorFactory, distinct, table, columns, selection,
+ selectionArgs, groupBy, having, orderBy, limit, null);
+ }
+
+ /**
+ * Query the given URL, returning a {@link Cursor} over the result set.
+ *
+ * @param cursorFactory the cursor factory to use, or null for the default factory
+ * @param distinct true if you want each row to be unique, false otherwise.
+ * @param table The table name to compile the query against.
+ * @param columns A list of which columns to return. Passing null will
+ * return all columns, which is discouraged to prevent reading
+ * data from storage that isn't going to be used.
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null
+ * will return all rows for the given table.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @param groupBy A filter declaring how to group rows, formatted as an SQL
+ * GROUP BY clause (excluding the GROUP BY itself). Passing null
+ * will cause the rows to not be grouped.
+ * @param having A filter declare which row groups to include in the cursor,
+ * if row grouping is being used, formatted as an SQL HAVING
+ * clause (excluding the HAVING itself). Passing null will cause
+ * all row groups to be included, and is required when row
+ * grouping is not being used.
+ * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
+ * (excluding the ORDER BY itself). Passing null will use the
+ * default sort order, which may be unordered.
+ * @param limit Limits the number of rows returned by the query,
+ * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ * @see Cursor
+ */
+ public Cursor queryWithFactory(CursorFactory cursorFactory,
+ boolean distinct, String table, String[] columns,
+ String selection, String[] selectionArgs, String groupBy,
+ String having, String orderBy, String limit, CancelationSignal cancelationSignal) {
+ throwIfNotOpen(); // fail fast
String sql = SQLiteQueryBuilder.buildQueryString(
distinct, table, columns, selection, groupBy, having, orderBy, limit);
- return rawQueryWithFactory(
- cursorFactory, sql, selectionArgs, findEditTable(table));
+ return rawQueryWithFactory(cursorFactory, sql, selectionArgs,
+ findEditTable(table), cancelationSignal);
}
/**
@@ -1535,7 +1153,25 @@ public class SQLiteDatabase extends SQLiteClosable {
* {@link Cursor}s are not synchronized, see the documentation for more details.
*/
public Cursor rawQuery(String sql, String[] selectionArgs) {
- return rawQueryWithFactory(null, sql, selectionArgs, null);
+ return rawQueryWithFactory(null, sql, selectionArgs, null, null);
+ }
+
+ /**
+ * Runs the provided SQL and returns a {@link Cursor} over the result set.
+ *
+ * @param sql the SQL query. The SQL string must not be ; terminated
+ * @param selectionArgs You may include ?s in where clause in the query,
+ * which will be replaced by the values from selectionArgs. The
+ * values will be bound as Strings.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ */
+ public Cursor rawQuery(String sql, String[] selectionArgs,
+ CancelationSignal cancelationSignal) {
+ return rawQueryWithFactory(null, sql, selectionArgs, null, cancelationSignal);
}
/**
@@ -1553,21 +1189,33 @@ public class SQLiteDatabase extends SQLiteClosable {
public Cursor rawQueryWithFactory(
CursorFactory cursorFactory, String sql, String[] selectionArgs,
String editTable) {
- verifyDbIsOpen();
- BlockGuard.getThreadPolicy().onReadFromDisk();
+ return rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable, null);
+ }
- SQLiteDatabase db = getDbConnection(sql);
- SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable);
+ /**
+ * Runs the provided SQL and returns a cursor over the result set.
+ *
+ * @param cursorFactory the cursor factory to use, or null for the default factory
+ * @param sql the SQL query. The SQL string must not be ; terminated
+ * @param selectionArgs You may include ?s in where clause in the query,
+ * which will be replaced by the values from selectionArgs. The
+ * values will be bound as Strings.
+ * @param editTable the name of the first table, which is editable
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ */
+ public Cursor rawQueryWithFactory(
+ CursorFactory cursorFactory, String sql, String[] selectionArgs,
+ String editTable, CancelationSignal cancelationSignal) {
+ throwIfNotOpen(); // fail fast
- Cursor cursor = null;
- try {
- cursor = driver.query(
- cursorFactory != null ? cursorFactory : mFactory,
- selectionArgs);
- } finally {
- releaseDbConnection(db);
- }
- return cursor;
+ SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable,
+ cancelationSignal);
+ return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory,
+ selectionArgs);
}
/**
@@ -1716,9 +1364,6 @@ public class SQLiteDatabase extends SQLiteClosable {
SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeInsert();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1739,9 +1384,6 @@ public class SQLiteDatabase extends SQLiteClosable {
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1808,9 +1450,6 @@ public class SQLiteDatabase extends SQLiteClosable {
SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
@@ -1891,266 +1530,107 @@ public class SQLiteDatabase extends SQLiteClosable {
private int executeSql(String sql, Object[] bindArgs) throws SQLException {
if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
- disableWriteAheadLogging();
- mHasAttachedDbs = true;
+ boolean disableWal = false;
+ synchronized (mLock) {
+ if (!mHasAttachedDbsLocked) {
+ mHasAttachedDbsLocked = true;
+ disableWal = true;
+ }
+ }
+ if (disableWal) {
+ disableWriteAheadLogging();
+ }
}
+
SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
try {
return statement.executeUpdateDelete();
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
} finally {
statement.close();
}
}
- @Override
- protected void finalize() throws Throwable {
- try {
- if (isOpen()) {
- Log.e(TAG, "close() was never explicitly called on database '" +
- mPath + "' ", mStackTrace);
- closeClosable();
- onAllReferencesReleased();
- releaseCustomFunctions();
- }
- } finally {
- super.finalize();
- }
- }
-
/**
- * Private constructor.
+ * Returns true if the database is opened as read only.
*
- * @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.
+ * @return True if database is opened as read only.
*/
- private SQLiteDatabase(String path, CursorFactory factory, int flags,
- DatabaseErrorHandler errorHandler, short connectionNum) {
- if (path == null) {
- throw new IllegalArgumentException("path should not be null");
+ public boolean isReadOnly() {
+ synchronized (mLock) {
+ return isReadOnlyLocked();
}
- setMaxSqlCacheSize(DEFAULT_SQL_CACHE_SIZE);
- mFlags = flags;
- mPath = path;
- mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
- mFactory = factory;
- mPrograms = new WeakHashMap<SQLiteClosable,Object>();
- // 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;
- /* sqlite soft heap limit http://www.sqlite.org/c3ref/soft_heap_limit64.html
- * set it to 4 times the default cursor window size.
- * TODO what is an appropriate value, considering the WAL feature which could burn
- * a lot of memory with many connections to the database. needs testing to figure out
- * optimal value for this.
- */
- int limit = Resources.getSystem().getInteger(
- com.android.internal.R.integer.config_cursorWindowSize) * 1024 * 4;
- native_setSqliteSoftHeapLimit(limit);
+ }
+
+ private boolean isReadOnlyLocked() {
+ return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY;
}
/**
- * return whether the DB is opened as read only.
- * @return true if DB is opened as read only
+ * Returns true if the database is in-memory db.
+ *
+ * @return True if the database is in-memory.
+ * @hide
*/
- public boolean isReadOnly() {
- return (mFlags & OPEN_READ_MASK) == OPEN_READONLY;
+ public boolean isInMemoryDatabase() {
+ synchronized (mLock) {
+ return mConfigurationLocked.isInMemoryDb();
+ }
}
/**
- * @return true if the DB is currently open (has not been closed)
+ * Returns true if the database is currently open.
+ *
+ * @return True if the database is currently open (has not been closed).
*/
public boolean isOpen() {
- return mNativeHandle != 0;
+ synchronized (mLock) {
+ return mConnectionPoolLocked != null;
+ }
}
+ /**
+ * Returns true if the new version code is greater than the current database version.
+ *
+ * @param newVersion The new version code.
+ * @return True if the new version code is greater than the current database version.
+ */
public boolean needUpgrade(int newVersion) {
return newVersion > getVersion();
}
/**
- * Getter for the path to the database file.
+ * Gets the path to the database file.
*
- * @return the path to our database file.
+ * @return The path to the database file.
*/
public final String getPath() {
- return mPath;
- }
-
- /* package */ void logTimeStat(String sql, long beginMillis) {
- if (ENABLE_DB_SAMPLE) {
- logTimeStat(sql, beginMillis, null);
- }
- }
-
- private void logTimeStat(String sql, long beginMillis, String prefix) {
- // Sample fast queries in proportion to the time taken.
- // Quantize the % first, so the logged sampling probability
- // exactly equals the actual sampling rate for this query.
-
- int samplePercent;
- long durationMillis = SystemClock.uptimeMillis() - beginMillis;
- if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) {
- // The common case is locks being uncontended. Don't log those,
- // even at 1%, which is our default below.
- return;
- }
- if (sQueryLogTimeInMillis == 0) {
- sQueryLogTimeInMillis = SystemProperties.getInt("db.db_operation.threshold_ms", 500);
- }
- if (durationMillis >= sQueryLogTimeInMillis) {
- samplePercent = 100;
- } else {
- samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1;
- if (mRandom.nextInt(100) >= samplePercent) return;
- }
-
- // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do
- // it here so we avoid allocating in the common case.
- if (prefix != null) {
- sql = prefix + sql;
+ synchronized (mLock) {
+ return mConfigurationLocked.path;
}
- if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH);
-
- // ActivityThread.currentPackageName() only returns non-null if the
- // current thread is an application main thread. This parameter tells
- // us whether an event loop is blocked, and if so, which app it is.
- //
- // Sadly, there's no fast way to determine app name if this is *not* a
- // main thread, or when we are invoked via Binder (e.g. ContentProvider).
- // Hopefully the full path to the database will be informative enough.
-
- String blockingPackage = AppGlobals.getInitialPackage();
- if (blockingPackage == null) blockingPackage = "";
-
- EventLog.writeEvent(
- EVENT_DB_OPERATION,
- getPathForLogs(),
- sql,
- durationMillis,
- blockingPackage,
- samplePercent);
- }
-
- /**
- * Removes email addresses from database filenames before they're
- * logged to the EventLog where otherwise apps could potentially
- * read them.
- */
- private String getPathForLogs() {
- if (mPathForLogs != null) {
- return mPathForLogs;
- }
- if (mPath == null) {
- return null;
- }
- if (mPath.indexOf('@') == -1) {
- mPathForLogs = mPath;
- } else {
- mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY");
- }
- return mPathForLogs;
}
/**
* Sets the locale for this database. Does nothing if this database has
* the NO_LOCALIZED_COLLATORS flag set or was opened read only.
+ *
+ * @param locale The new locale.
+ *
* @throws SQLException if the locale could not be set. The most common reason
* for this is that there is no collator available for the locale you requested.
* In this case the database remains unchanged.
*/
public void setLocale(Locale locale) {
- lock();
- try {
- native_setLocale(locale.toString(), mFlags);
- } finally {
- unlock();
+ if (locale == null) {
+ throw new IllegalArgumentException("locale must not be null.");
}
- }
- /* package */ void verifyDbIsOpen() {
- if (!isOpen()) {
- throw new IllegalStateException("database " + getPath() + " (conn# " +
- mConnectionNum + ") already closed");
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.locale = locale;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
}
- /* package */ void verifyLockOwner() {
- verifyDbIsOpen();
- if (mLockingEnabled && !isDbLockedByCurrentThread()) {
- throw new IllegalStateException("Don't have database lock!");
- }
- }
-
- /**
- * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the
- * cache of compiledQueries attached to 'this'.
- * <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 */ synchronized void addToCompiledQueries(
- String sql, SQLiteCompiledSql compiledStatement) {
- // don't insert the new mapping if a mapping already exists
- if (mCompiledQueries.get(sql) != null) {
- return;
- }
-
- int maxCacheSz = (mConnectionNum == 0) ? mCompiledQueries.maxSize() :
- mParentConnObj.mCompiledQueries.maxSize();
-
- if (SQLiteDebug.DEBUG_SQL_CACHE) {
- boolean printWarning = (mConnectionNum == 0)
- ? (!mCacheFullWarning && mCompiledQueries.size() == maxCacheSz)
- : (!mParentConnObj.mCacheFullWarning &&
- mParentConnObj.mCompiledQueries.size() == maxCacheSz);
- if (printWarning) {
- /*
- * cache size is not enough for this app. log a warning.
- * chances are it is NOT using ? for bindargs - or cachesize is too small.
- */
- Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " +
- getPath() + ". Use setMaxSqlCacheSize() to increase cachesize. ");
- mCacheFullWarning = true;
- Log.d(TAG, "Here are the SQL statements in Cache of database: " + mPath);
- for (String s : mCompiledQueries.snapshot().keySet()) {
- Log.d(TAG, "Sql statement in Cache: " + s);
- }
- }
- }
- /* add the given SQLiteCompiledSql compiledStatement to cache.
- * no need to worry about the cache size - because {@link #mCompiledQueries}
- * self-limits its size.
- */
- mCompiledQueries.put(sql, compiledStatement);
- }
-
- /** package-level access for testing purposes */
- /* package */ synchronized void deallocCachedSqlStatements() {
- for (SQLiteCompiledSql compiledSql : mCompiledQueries.snapshot().values()) {
- compiledSql.releaseSqlStatement();
- }
- mCompiledQueries.evictAll();
- }
-
- /**
- * From the compiledQueries cache, returns the compiled-statement-id for the given SQL.
- * Returns null, if not found in the cache.
- */
- /* package */ synchronized SQLiteCompiledSql getCompiledStatementForSql(String sql) {
- return mCompiledQueries.get(sql);
- }
-
/**
* 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).
@@ -2162,113 +1642,19 @@ public class SQLiteDatabase extends SQLiteClosable {
* 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.
+ * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE}.
*/
public void setMaxSqlCacheSize(int cacheSize) {
- synchronized (this) {
- LruCache<String, SQLiteCompiledSql> oldCompiledQueries = mCompiledQueries;
- if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
- throw new IllegalStateException(
- "expected value between 0 and " + MAX_SQL_CACHE_SIZE);
- } else if (oldCompiledQueries != null && cacheSize < oldCompiledQueries.maxSize()) {
- throw new IllegalStateException("cannot set cacheSize to a value less than the "
- + "value set with previous setMaxSqlCacheSize() call.");
- }
- mCompiledQueries = new LruCache<String, SQLiteCompiledSql>(cacheSize) {
- @Override
- protected void entryRemoved(boolean evicted, String key, SQLiteCompiledSql oldValue,
- SQLiteCompiledSql newValue) {
- verifyLockOwner();
- oldValue.releaseIfNotInUse();
- }
- };
- if (oldCompiledQueries != null) {
- for (Map.Entry<String, SQLiteCompiledSql> entry
- : oldCompiledQueries.snapshot().entrySet()) {
- mCompiledQueries.put(entry.getKey(), entry.getValue());
- }
- }
- }
- }
-
- /* package */ synchronized boolean isInStatementCache(String sql) {
- return mCompiledQueries.get(sql) != null;
- }
-
- /* package */ synchronized void releaseCompiledSqlObj(
- String sql, SQLiteCompiledSql compiledSql) {
- if (mCompiledQueries.get(sql) == compiledSql) {
- // it is in cache - reset its inUse flag
- compiledSql.release();
- } else {
- // it is NOT in cache. finalize it.
- compiledSql.releaseSqlStatement();
- }
- }
-
- private synchronized int getCacheHitNum() {
- return mCompiledQueries.hitCount();
- }
-
- private synchronized int getCacheMissNum() {
- return mCompiledQueries.missCount();
- }
-
- private synchronized int getCachesize() {
- return mCompiledQueries.size();
- }
-
- /* 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);
- }
- }
-
- /* package */ boolean isInQueueOfStatementsToBeFinalized(int id) {
- if (!isOpen()) {
- // database already closed. this statement will already have been finalized.
- // return true so that the caller doesn't have to worry about finalizing this statement.
- return true;
- }
- synchronized(mClosedStatementIds) {
- return mClosedStatementIds.contains(id);
+ if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
+ throw new IllegalStateException(
+ "expected value between 0 and " + MAX_SQL_CACHE_SIZE);
}
- }
- /* package */ 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();
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+ mConfigurationLocked.maxSqlCacheSize = cacheSize;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
- // finalize all the statements from the copied list
- int size = list.size();
- for (int i = 0; i < size; i++) {
- native_finalize(list.get(i));
- }
- }
-
- /**
- * for testing only
- */
- /* package */ ArrayList<Integer> getQueuedUpStmtList() {
- return mClosedStatementIds;
}
/**
@@ -2314,37 +1700,43 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return true if write-ahead-logging is set. false otherwise
*/
public boolean enableWriteAheadLogging() {
- // make sure the database is not READONLY. WAL doesn't make sense for readonly-databases.
- if (isReadOnly()) {
- return false;
- }
- // acquire lock - no that no other thread is enabling WAL at the same time
- lock();
- try {
- if (mConnectionPool != null) {
- // already enabled
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+
+ if (mIsWALEnabledLocked) {
return true;
}
- if (mPath.equalsIgnoreCase(MEMORY_DB_PATH)) {
+
+ if (isReadOnlyLocked()) {
+ // WAL doesn't make sense for readonly-databases.
+ // TODO: True, but connection pooling does still make sense...
+ return false;
+ }
+
+ if (mConfigurationLocked.isInMemoryDb()) {
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 (mHasAttachedDbs) {
+ if (mHasAttachedDbsLocked) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG,
- "this database: " + mPath + " has attached databases. can't enable WAL.");
+ Log.d(TAG, "this database: " + mConfigurationLocked.label
+ + " has attached databases. can't enable WAL.");
}
return false;
}
- mConnectionPool = new DatabaseConnectionPool(this);
- setJournalMode(mPath, "WAL");
- return true;
- } finally {
- unlock();
+
+ mIsWALEnabledLocked = true;
+ mConfigurationLocked.maxConnectionPoolSize = Math.max(2,
+ Resources.getSystem().getInteger(
+ com.android.internal.R.integer.db_connection_pool_size));
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
}
+
+ setJournalMode("WAL");
+ return true;
}
/**
@@ -2352,176 +1744,66 @@ public class SQLiteDatabase extends SQLiteClosable {
* @hide
*/
public void disableWriteAheadLogging() {
- // grab database lock so that writeAheadLogging is not disabled from 2 different threads
- // at the same time
- lock();
- try {
- if (mConnectionPool == null) {
- return; // already disabled
- }
- mConnectionPool.close();
- setJournalMode(mPath, "TRUNCATE");
- mConnectionPool = null;
- } finally {
- unlock();
- }
- }
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
- /* package */ SQLiteDatabase getDatabaseHandle(String sql) {
- if (isPooledConnection()) {
- // this is a pooled database connection
- // use it if it is open AND if I am not currently part of a transaction
- if (isOpen() && !amIInTransaction()) {
- // TODO: use another connection from the pool
- // if this connection is currently in use by some other thread
- // AND if there are free connections in the pool
- return this;
- } else {
- // the pooled connection is not open! could have been closed either due
- // to corruption on this or some other connection to the database
- // OR, maybe the connection pool is disabled after this connection has been
- // allocated to me. try to get some other pooled or main database connection
- return getParentDbConnObj().getDbConnection(sql);
+ if (!mIsWALEnabledLocked) {
+ return;
}
- } else {
- // this is NOT a pooled connection. can we get one?
- return getDbConnection(sql);
- }
- }
-
- /* package */ SQLiteDatabase createPoolConnection(short connectionNum) {
- SQLiteDatabase db = openDatabase(mPath, mFactory, mFlags, mErrorHandler, connectionNum);
- db.mParentConnObj = this;
- return db;
- }
- private synchronized SQLiteDatabase getParentDbConnObj() {
- return mParentConnObj;
- }
+ mIsWALEnabledLocked = false;
+ mConfigurationLocked.maxConnectionPoolSize = 1;
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+ }
- private boolean isPooledConnection() {
- return this.mConnectionNum > 0;
+ setJournalMode("TRUNCATE");
}
- /* package */ SQLiteDatabase getDbConnection(String sql) {
- verifyDbIsOpen();
- // this method should always be called with main database connection handle.
- // the only time when it is called with pooled database connection handle is
- // corruption occurs while trying to open a pooled database connection handle.
- // in that case, simply return 'this' handle
- if (isPooledConnection()) {
- return this;
+ /**
+ * Collect statistics about all open databases in the current process.
+ * Used by bug report.
+ */
+ static ArrayList<DbStats> getDbStats() {
+ ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.collectDbStats(dbStatsList);
}
+ return dbStatsList;
+ }
- // use the current connection handle if
- // 1. if the caller is part of the ongoing transaction, if any
- // 2. OR, if there is NO connection handle pool setup
- if (amIInTransaction() || mConnectionPool == null) {
- return this;
- } else {
- // get a connection handle from the pool
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- assert mConnectionPool != null;
- Log.i(TAG, mConnectionPool.toString());
+ private void collectDbStats(ArrayList<DbStats> dbStatsList) {
+ synchronized (mLock) {
+ if (mConnectionPoolLocked != null) {
+ mConnectionPoolLocked.collectDbStats(dbStatsList);
}
- return mConnectionPool.get(sql);
}
}
- 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());
+ private static ArrayList<SQLiteDatabase> getActiveDatabases() {
+ ArrayList<SQLiteDatabase> databases = new ArrayList<SQLiteDatabase>();
+ synchronized (sActiveDatabases) {
+ databases.addAll(sActiveDatabases.keySet());
}
- mConnectionPool.release(db);
+ return databases;
}
/**
- * this method is used to collect data about ALL open databases in the current process.
- * bugreport is a user of this data.
+ * Dump detailed information about all open databases in the current process.
+ * Used by bug report.
*/
- /* package */ static ArrayList<DbStats> getDbStats() {
- ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
- // make a local copy of mActiveDatabases - so that this method is not competing
- // for synchronization lock on mActiveDatabases
- ArrayList<WeakReference<SQLiteDatabase>> tempList;
- synchronized(mActiveDatabases) {
- tempList = (ArrayList<WeakReference<SQLiteDatabase>>)mActiveDatabases.clone();
+ static void dumpAll(Printer printer, boolean verbose) {
+ for (SQLiteDatabase db : getActiveDatabases()) {
+ db.dump(printer, verbose);
}
- for (WeakReference<SQLiteDatabase> w : tempList) {
- SQLiteDatabase db = w.get();
- if (db == null || !db.isOpen()) {
- continue;
- }
+ }
- try {
- // 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);
-
- // get list of attached dbs and for each db, get its size and pagesize
- List<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.getCacheHitNum(), db.getCacheMissNum(),
- db.getCachesize()));
- }
- }
- // if there are pooled connections, return the cache stats for them also.
- // while we are trying to query the pooled connections for stats, some other thread
- // could be disabling conneciton pool. so, grab a reference to the connection pool.
- DatabaseConnectionPool connPool = db.mConnectionPool;
- if (connPool != null) {
- for (SQLiteDatabase pDb : connPool.getConnectionList()) {
- dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") "
- + lastnode, 0, 0, 0, pDb.getCacheHitNum(),
- pDb.getCacheMissNum(), pDb.getCachesize()));
- }
- }
- } catch (SQLiteException e) {
- // ignore. we don't care about exceptions when we are taking adb
- // bugreport!
+ private void dump(Printer printer, boolean verbose) {
+ synchronized (mLock) {
+ if (mConnectionPoolLocked != null) {
+ printer.println("");
+ mConnectionPoolLocked.dump(printer, verbose);
}
}
- return dbStatsList;
}
/**
@@ -2532,23 +1814,27 @@ public class SQLiteDatabase extends SQLiteClosable {
* is not open.
*/
public List<Pair<String, String>> getAttachedDbs() {
- if (!isOpen()) {
- return null;
- }
ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
- if (!mHasAttachedDbs) {
- // No attached databases.
- // There is a small window where attached databases exist but this flag is not set yet.
- // This can occur when this thread is in a race condition with another thread
- // that is executing the SQL statement: "attach database <blah> as <foo>"
- // If this thread is NOT ok with such a race condition (and thus possibly not receive
- // the entire list of attached databases), then the caller should ensure that no thread
- // is executing any SQL statements while a thread is calling this method.
- // Typically, this method is called when 'adb bugreport' is done or the caller wants to
- // collect stats on the database and all its attached databases.
- attachedDbs.add(new Pair<String, String>("main", mPath));
- return attachedDbs;
+ synchronized (mLock) {
+ if (mConnectionPoolLocked == null) {
+ return null; // not open
+ }
+
+ if (!mHasAttachedDbsLocked) {
+ // No attached databases.
+ // There is a small window where attached databases exist but this flag is not
+ // set yet. This can occur when this thread is in a race condition with another
+ // thread that is executing the SQL statement: "attach database <blah> as <foo>"
+ // If this thread is NOT ok with such a race condition (and thus possibly not
+ // receivethe entire list of attached databases), then the caller should ensure
+ // that no thread is executing any SQL statements while a thread is calling this
+ // method. Typically, this method is called when 'adb bugreport' is done or the
+ // caller wants to collect stats on the database and all its attached databases.
+ attachedDbs.add(new Pair<String, String>("main", mConfigurationLocked.path));
+ return attachedDbs;
+ }
}
+
// has attached databases. query sqlite to get the list of attached databases.
Cursor c = null;
try {
@@ -2583,7 +1869,8 @@ public class SQLiteDatabase extends SQLiteClosable {
* false otherwise.
*/
public boolean isDatabaseIntegrityOk() {
- verifyDbIsOpen();
+ throwIfNotOpen(); // fail fast
+
List<Pair<String, String>> attachedDbs = null;
try {
attachedDbs = getAttachedDbs();
@@ -2594,8 +1881,9 @@ public class SQLiteDatabase extends SQLiteClosable {
} catch (SQLiteException e) {
// can't get attachedDb list. do integrity check on the main database
attachedDbs = new ArrayList<Pair<String, String>>();
- attachedDbs.add(new Pair<String, String>("main", this.mPath));
+ attachedDbs.add(new Pair<String, String>("main", getPath()));
}
+
for (int i = 0; i < attachedDbs.size(); i++) {
Pair<String, String> p = attachedDbs.get(i);
SQLiteStatement prog = null;
@@ -2615,59 +1903,64 @@ public class SQLiteDatabase extends SQLiteClosable {
}
/**
- * Native call to open the database.
+ * Prevent other threads from using the database's primary connection.
*
- * @param path The full path to the database
- */
- private native void dbopen(String path, int flags);
-
- /**
- * Native call to setup tracing of all SQL statements
+ * This method is only used by {@link SQLiteOpenHelper} when transitioning from
+ * a readable to a writable database. It should not be used in any other way.
*
- * @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.
+ * @see #unlockPrimaryConnection()
*/
- private native void enableSqlTracing(String path, short connectionNum);
+ void lockPrimaryConnection() {
+ getThreadSession().beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED,
+ null, SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY, null);
+ }
/**
- * 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
- * are executed.
+ * Allow other threads to use the database's primary connection.
*
- * @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.
+ * @see #lockPrimaryConnection()
*/
- private native void enableSqlProfiling(String path, short connectionNum);
+ void unlockPrimaryConnection() {
+ getThreadSession().endTransaction(null);
+ }
- /**
- * Native call to set the locale. {@link #lock} must be held when calling
- * this method.
- * @throws SQLException
- */
- private native void native_setLocale(String loc, int flags);
+ @Override
+ public String toString() {
+ return "SQLiteDatabase: " + getPath();
+ }
- /**
- * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here
- * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html
- * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED
- */
- private native int native_getDbLookaside();
+ private void throwIfNotOpen() {
+ synchronized (mConnectionPoolLocked) {
+ throwIfNotOpenLocked();
+ }
+ }
+
+ private void throwIfNotOpenLocked() {
+ if (mConnectionPoolLocked == null) {
+ throw new IllegalStateException("The database '" + mConfigurationLocked.label
+ + "' is not open.");
+ }
+ }
/**
- * finalizes the given statement id.
- *
- * @param statementId statement to be finzlied by sqlite
+ * Used to allow returning sub-classes of {@link Cursor} when calling query.
*/
- private final native void native_finalize(int statementId);
+ public interface CursorFactory {
+ /**
+ * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}.
+ */
+ public Cursor newCursor(SQLiteDatabase db,
+ SQLiteCursorDriver masterQuery, String editTable,
+ SQLiteQuery query);
+ }
/**
- * set sqlite soft heap limit
- * http://www.sqlite.org/c3ref/soft_heap_limit64.html
+ * A callback interface for a custom sqlite3 function.
+ * This can be used to create a function that can be called from
+ * sqlite3 database triggers.
+ * @hide
*/
- private native void native_setSqliteSoftHeapLimit(int softHeapLimit);
+ public interface CustomFunction {
+ public void callback(String[] args);
+ }
}
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
new file mode 100644
index 0000000..bc79ad3
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Describes how to configure a database.
+ * <p>
+ * The purpose of this object is to keep track of all of the little
+ * configuration settings that are applied to a database after it
+ * is opened so that they can be applied to all connections in the
+ * connection pool uniformly.
+ * </p><p>
+ * Each connection maintains its own copy of this object so it can
+ * keep track of which settings have already been applied.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteDatabaseConfiguration {
+ // The pattern we use to strip email addresses from database paths
+ // when constructing a label to use in log messages.
+ private static final Pattern EMAIL_IN_DB_PATTERN =
+ Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
+
+ /**
+ * Special path used by in-memory databases.
+ */
+ public static final String MEMORY_DB_PATH = ":memory:";
+
+ /**
+ * The database path.
+ */
+ public final String path;
+
+ /**
+ * The flags used to open the database.
+ */
+ public final int openFlags;
+
+ /**
+ * The label to use to describe the database when it appears in logs.
+ * This is derived from the path but is stripped to remove PII.
+ */
+ public final String label;
+
+ /**
+ * The maximum number of connections to retain in the connection pool.
+ * Must be at least 1.
+ *
+ * Default is 1.
+ */
+ public int maxConnectionPoolSize;
+
+ /**
+ * The maximum size of the prepared statement cache for each database connection.
+ * Must be non-negative.
+ *
+ * Default is 25.
+ */
+ public int maxSqlCacheSize;
+
+ /**
+ * The database locale.
+ *
+ * Default is the value returned by {@link Locale#getDefault()}.
+ */
+ public Locale locale;
+
+ /**
+ * The custom functions to register.
+ */
+ public final ArrayList<SQLiteCustomFunction> customFunctions =
+ new ArrayList<SQLiteCustomFunction>();
+
+ /**
+ * Creates a database configuration with the required parameters for opening a
+ * database and default values for all other parameters.
+ *
+ * @param path The database path.
+ * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}.
+ */
+ public SQLiteDatabaseConfiguration(String path, int openFlags) {
+ if (path == null) {
+ throw new IllegalArgumentException("path must not be null.");
+ }
+
+ this.path = path;
+ this.openFlags = openFlags;
+ label = stripPathForLogs(path);
+
+ // Set default values for optional parameters.
+ maxConnectionPoolSize = 1;
+ maxSqlCacheSize = 25;
+ locale = Locale.getDefault();
+ }
+
+ /**
+ * Creates a database configuration as a copy of another configuration.
+ *
+ * @param other The other configuration.
+ */
+ public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null.");
+ }
+
+ this.path = other.path;
+ this.openFlags = other.openFlags;
+ this.label = other.label;
+ updateParametersFrom(other);
+ }
+
+ /**
+ * Updates the non-immutable parameters of this configuration object
+ * from the other configuration object.
+ *
+ * @param other The object from which to copy the parameters.
+ */
+ public void updateParametersFrom(SQLiteDatabaseConfiguration other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null.");
+ }
+ if (!path.equals(other.path) || openFlags != other.openFlags) {
+ throw new IllegalArgumentException("other configuration must refer to "
+ + "the same database.");
+ }
+
+ maxConnectionPoolSize = other.maxConnectionPoolSize;
+ maxSqlCacheSize = other.maxSqlCacheSize;
+ locale = other.locale;
+ customFunctions.clear();
+ customFunctions.addAll(other.customFunctions);
+ }
+
+ /**
+ * Returns true if the database is in-memory.
+ * @return True if the database is in-memory.
+ */
+ public boolean isInMemoryDb() {
+ return path.equalsIgnoreCase(MEMORY_DB_PATH);
+ }
+
+ private static String stripPathForLogs(String path) {
+ if (path.indexOf('@') == -1) {
+ return path;
+ }
+ return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY");
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index cc057e0..204483d 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import android.os.Build;
import android.os.SystemProperties;
import android.util.Log;
+import android.util.Printer;
/**
* Provides debugging info about all SQLite databases running in the current process.
@@ -28,6 +29,14 @@ import android.util.Log;
* {@hide}
*/
public final class SQLiteDebug {
+ private static native void nativeGetPagerStats(PagerStats stats);
+
+ /**
+ * Controls the printing of informational SQL log messages.
+ */
+ public static final boolean DEBUG_SQL_LOG =
+ Log.isLoggable("SQLiteLog", Log.VERBOSE);
+
/**
* Controls the printing of SQL statements as they are executed.
*/
@@ -42,31 +51,6 @@ public final class SQLiteDebug {
Log.isLoggable("SQLiteTime", Log.VERBOSE);
/**
- * Controls the printing of compiled-sql-statement cache stats.
- */
- public static final boolean DEBUG_SQL_CACHE =
- Log.isLoggable("SQLiteCompiledSql", Log.VERBOSE);
-
- /**
- * Controls the stack trace reporting of active cursors being
- * finalized.
- */
- public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION =
- Log.isLoggable("SQLiteCursorClosing", Log.VERBOSE);
-
- /**
- * Controls the tracking of time spent holding the database lock.
- */
- public static final boolean DEBUG_LOCK_TIME_TRACKING =
- Log.isLoggable("SQLiteLockTime", Log.VERBOSE);
-
- /**
- * Controls the printing of stack traces when tracking the time spent holding the database lock.
- */
- public static final boolean DEBUG_LOCK_TIME_TRACKING_STACK_TRACE =
- Log.isLoggable("SQLiteLockStackTrace", Log.VERBOSE);
-
- /**
* True to enable database performance testing instrumentation.
* @hide
*/
@@ -91,30 +75,9 @@ public final class SQLiteDebug {
/**
* Contains statistics about the active pagers in the current process.
*
- * @see #getPagerStats(PagerStats)
+ * @see #nativeGetPagerStats(PagerStats)
*/
public static class PagerStats {
- /** The total number of bytes in all pagers in the current process
- * @deprecated not used any longer
- */
- @Deprecated
- public long totalBytes;
- /** The number of bytes in referenced pages in all pagers in the current process
- * @deprecated not used any longer
- * */
- @Deprecated
- public long referencedBytes;
- /** The number of bytes in all database files opened in the current process
- * @deprecated not used any longer
- */
- @Deprecated
- public long databaseBytes;
- /** The number of pagers opened in the current process
- * @deprecated not used any longer
- */
- @Deprecated
- public int numPagers;
-
/** the current amount of memory checked out by sqlite using sqlite3_malloc().
* documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
*/
@@ -127,7 +90,7 @@ public final class SQLiteDebug {
* that overflowed because no space was left in the page cache.
* documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
*/
- public int pageCacheOverflo;
+ public int pageCacheOverflow;
/** records the largest memory allocation request handed to sqlite3.
* documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html
@@ -175,52 +138,24 @@ public final class SQLiteDebug {
*/
public static PagerStats getDatabaseInfo() {
PagerStats stats = new PagerStats();
- getPagerStats(stats);
+ nativeGetPagerStats(stats);
stats.dbStats = SQLiteDatabase.getDbStats();
return stats;
}
/**
- * Gathers statistics about all pagers in the current process.
- */
- public static native void getPagerStats(PagerStats stats);
-
- /**
- * Returns the size of the SQLite heap.
- * @return The size of the SQLite heap in bytes.
- */
- public static native long getHeapSize();
-
- /**
- * Returns the amount of allocated memory in the SQLite heap.
- * @return The allocated size in bytes.
- */
- public static native long getHeapAllocatedSize();
-
- /**
- * Returns the amount of free memory in the SQLite heap.
- * @return The freed size in bytes.
+ * Dumps detailed information about all databases used by the process.
+ * @param printer The printer for dumping database state.
+ * @param args Command-line arguments supplied to dumpsys dbinfo
*/
- public static native long getHeapFreeSize();
-
- /**
- * Determines the number of dirty belonging to the SQLite
- * heap segments of this process. pages[0] returns the number of
- * shared pages, pages[1] returns the number of private pages
- */
- public static native void getHeapDirtyPages(int[] pages);
-
- private static int sNumActiveCursorsFinalized = 0;
-
- /**
- * Returns the number of active cursors that have been finalized. This depends on the GC having
- * run but is still useful for tests.
- */
- public static int getNumActiveCursorsFinalized() {
- return sNumActiveCursorsFinalized;
- }
+ public static void dump(Printer printer, String[] args) {
+ boolean verbose = false;
+ for (String arg : args) {
+ if (arg.equals("-v")) {
+ verbose = true;
+ }
+ }
- static synchronized void notifyActiveCursorFinalized() {
- sNumActiveCursorsFinalized++;
+ SQLiteDatabase.dumpAll(printer, verbose);
}
}
diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
index a5e762e..c490dc6 100644
--- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
+++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -16,6 +16,7 @@
package android.database.sqlite;
+import android.content.CancelationSignal;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
@@ -25,46 +26,42 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory;
* @hide
*/
public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
- private String mEditTable;
- private SQLiteDatabase mDatabase;
- private Cursor mCursor;
- private String mSql;
+ private final SQLiteDatabase mDatabase;
+ private final String mEditTable;
+ private final String mSql;
+ private final CancelationSignal mCancelationSignal;
private SQLiteQuery mQuery;
- public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) {
+ public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable,
+ CancelationSignal cancelationSignal) {
mDatabase = db;
mEditTable = editTable;
mSql = sql;
+ mCancelationSignal = cancelationSignal;
}
public Cursor query(CursorFactory factory, String[] selectionArgs) {
- // Compile the query
- SQLiteQuery query = null;
-
+ final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancelationSignal);
+ final Cursor cursor;
try {
- mDatabase.lock(mSql);
- mDatabase.closePendingStatements();
- query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
+ query.bindAllArgsAsStrings(selectionArgs);
- // Create the cursor
if (factory == null) {
- mCursor = new SQLiteCursor(this, mEditTable, query);
+ cursor = new SQLiteCursor(this, mEditTable, query);
} else {
- mCursor = factory.newCursor(mDatabase, this, mEditTable, query);
+ cursor = factory.newCursor(mDatabase, this, mEditTable, query);
}
-
- mQuery = query;
- query = null;
- return mCursor;
- } finally {
- // Make sure this object is cleaned up if something happens
- if (query != null) query.close();
- mDatabase.unlock();
+ } catch (RuntimeException ex) {
+ query.close();
+ throw ex;
}
+
+ mQuery = query;
+ return cursor;
}
public void cursorClosed() {
- mCursor = null;
+ // Do nothing
}
public void setBindArguments(String[] bindArgs) {
diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java
new file mode 100644
index 0000000..dbefd63
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteGlobal.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.os.StatFs;
+
+/**
+ * Provides access to SQLite functions that affect all database connection,
+ * such as memory management.
+ *
+ * The native code associated with SQLiteGlobal is also sets global configuration options
+ * using sqlite3_config() then calls sqlite3_initialize() to ensure that the SQLite
+ * library is properly initialized exactly once before any other framework or application
+ * code has a chance to run.
+ *
+ * Verbose SQLite logging is enabled if the "log.tag.SQLiteLog" property is set to "V".
+ * (per {@link SQLiteDebug#DEBUG_SQL_LOG}).
+ *
+ * @hide
+ */
+public final class SQLiteGlobal {
+ private static final String TAG = "SQLiteGlobal";
+
+ private static final Object sLock = new Object();
+ private static int sDefaultPageSize;
+
+ private static native int nativeReleaseMemory();
+
+ private SQLiteGlobal() {
+ }
+
+ /**
+ * Attempts to release memory by pruning the SQLite page cache and other
+ * internal data structures.
+ *
+ * @return The number of bytes that were freed.
+ */
+ public static int releaseMemory() {
+ return nativeReleaseMemory();
+ }
+
+ /**
+ * Gets the default page size to use when creating a database.
+ */
+ public static int getDefaultPageSize() {
+ synchronized (sLock) {
+ if (sDefaultPageSize == 0) {
+ sDefaultPageSize = new StatFs("/data").getBlockSize();
+ }
+ return sDefaultPageSize;
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java
index 56cf948..46d9369 100644
--- a/core/java/android/database/sqlite/SQLiteOpenHelper.java
+++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java
@@ -81,7 +81,8 @@ public abstract class SQLiteOpenHelper {
* @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
+ * {@link #onUpgrade} will be used to upgrade the database; if the database is
+ * newer, {@link #onDowngrade} will be used to downgrade the database
* @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database
* corruption.
*/
@@ -100,7 +101,7 @@ public abstract class SQLiteOpenHelper {
}
/**
- * Return the name of the SQLite database being opened, as given tp
+ * Return the name of the SQLite database being opened, as given to
* the constructor.
*/
public String getDatabaseName() {
@@ -143,12 +144,14 @@ public abstract class SQLiteOpenHelper {
// If we have a read-only database open, someone could be using it
// (though they shouldn't), which would cause a lock to be held on
// the file, and our attempts to open the database read-write would
- // fail waiting for the file lock. To prevent that, we acquire the
- // lock on the read-only database, which shuts out other users.
+ // fail waiting for the file lock. To prevent that, we acquire a lock
+ // on the read-only database, which shuts out other users.
boolean success = false;
SQLiteDatabase db = null;
- if (mDatabase != null) mDatabase.lock();
+ if (mDatabase != null) {
+ mDatabase.lockPrimaryConnection();
+ }
try {
mIsInitializing = true;
if (mName == null) {
@@ -185,11 +188,13 @@ public abstract class SQLiteOpenHelper {
if (success) {
if (mDatabase != null) {
try { mDatabase.close(); } catch (Exception e) { }
- mDatabase.unlock();
+ mDatabase.unlockPrimaryConnection();
}
mDatabase = db;
} else {
- if (mDatabase != null) mDatabase.unlock();
+ if (mDatabase != null) {
+ mDatabase.unlockPrimaryConnection();
+ }
if (db != null) db.close();
}
}
@@ -293,7 +298,7 @@ public abstract class SQLiteOpenHelper {
public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
/**
- * Called when the database needs to be downgraded. This is stricly similar to
+ * Called when the database needs to be downgraded. This is strictly similar to
* onUpgrade() method, but is called whenever current version is newer than requested one.
* However, this method is not abstract, so it is not mandatory for a customer to
* implement it. If not overridden, default implementation will reject downgrade and
diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java
index 89552dc..f3da2a6 100644
--- a/core/java/android/database/sqlite/SQLiteProgram.java
+++ b/core/java/android/database/sqlite/SQLiteProgram.java
@@ -16,241 +16,108 @@
package android.database.sqlite;
+import android.content.CancelationSignal;
import android.database.DatabaseUtils;
-import android.database.Cursor;
-import java.util.HashMap;
+import java.util.Arrays;
/**
* 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.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
*/
public abstract class SQLiteProgram extends SQLiteClosable {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private static final String TAG = "SQLiteProgram";
+ private final SQLiteDatabase mDatabase;
+ private final String mSql;
+ private final boolean mReadOnly;
+ private final String[] mColumnNames;
+ private final int mNumParameters;
+ private final Object[] mBindArgs;
- /** The database this program is compiled against.
- * @deprecated do not use this
- */
- @Deprecated
- protected SQLiteDatabase mDatabase;
-
- /** The SQL used to create this query */
- /* package */ final String mSql;
-
- /**
- * Native linkage, do not modify. This comes from the database and should not be modified
- * in here or in the native code.
- * @deprecated do not use this
- */
- @Deprecated
- protected int nHandle;
-
- /**
- * the SQLiteCompiledSql object for the given sql statement.
- */
- /* package */ SQLiteCompiledSql mCompiledSql;
-
- /**
- * SQLiteCompiledSql statement id is populated with the corresponding object from the above
- * member. This member is used by the native_bind_* methods
- * @deprecated do not use this
- */
- @Deprecated
- protected int nStatement;
-
- /**
- * 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
- */
- /* package */ HashMap<Integer, Object> mBindArgs = null;
- /* package */ final int mStatementType;
- /* package */ static final int STATEMENT_CACHEABLE = 16;
- /* package */ static final int STATEMENT_DONT_PREPARE = 32;
- /* package */ static final int STATEMENT_USE_POOLED_CONN = 64;
- /* package */ static final int STATEMENT_TYPE_MASK = 0x0f;
-
- /* package */ SQLiteProgram(SQLiteDatabase db, String sql) {
- this(db, sql, null, true);
- }
-
- /* package */ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs,
- boolean compileFlag) {
+ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs,
+ CancelationSignal cancelationSignalForPrepare) {
+ mDatabase = db;
mSql = sql.trim();
+
int n = DatabaseUtils.getSqlStatementType(mSql);
switch (n) {
- case DatabaseUtils.STATEMENT_UPDATE:
- mStatementType = n | STATEMENT_CACHEABLE;
- break;
- case DatabaseUtils.STATEMENT_SELECT:
- mStatementType = n | STATEMENT_CACHEABLE | STATEMENT_USE_POOLED_CONN;
- break;
case DatabaseUtils.STATEMENT_BEGIN:
case DatabaseUtils.STATEMENT_COMMIT:
case DatabaseUtils.STATEMENT_ABORT:
- mStatementType = n | STATEMENT_DONT_PREPARE;
+ mReadOnly = false;
+ mColumnNames = EMPTY_STRING_ARRAY;
+ mNumParameters = 0;
break;
+
default:
- mStatementType = n;
- }
- db.acquireReference();
- db.addSQLiteClosable(this);
- mDatabase = db;
- nHandle = db.mNativeHandle;
- if (bindArgs != null) {
- int size = bindArgs.length;
- for (int i = 0; i < size; i++) {
- this.addToBindArgs(i + 1, bindArgs[i]);
- }
- }
- if (compileFlag) {
- compileAndbindAllArgs();
+ boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT);
+ SQLiteStatementInfo info = new SQLiteStatementInfo();
+ db.getThreadSession().prepare(mSql,
+ db.getThreadDefaultConnectionFlags(assumeReadOnly),
+ cancelationSignalForPrepare, info);
+ mReadOnly = info.readOnly;
+ mColumnNames = info.columnNames;
+ mNumParameters = info.numParameters;
+ break;
}
- }
- private void compileSql() {
- // only cache CRUD statements
- if ((mStatementType & STATEMENT_CACHEABLE) == 0) {
- mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
- nStatement = mCompiledSql.nStatement;
- // since it is not in the cache, no need to acquire() it.
- return;
+ if (mNumParameters != 0) {
+ mBindArgs = new Object[mNumParameters];
+ } else {
+ mBindArgs = null;
}
- mCompiledSql = mDatabase.getCompiledStatementForSql(mSql);
- if (mCompiledSql == null) {
- // create a new compiled-sql obj
- 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();
- mDatabase.addToCompiledQueries(mSql, mCompiledSql);
- } else {
- // it is already in compiled-sql cache.
- // try to acquire the object.
- if (!mCompiledSql.acquire()) {
- int last = mCompiledSql.nStatement;
- // the SQLiteCompiledSql in cache is in use by some other SQLiteProgram object.
- // 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(mDatabase, mSql);
- // since it is not in the cache, no need to acquire() it.
+ if (bindArgs != null) {
+ if (bindArgs.length > mNumParameters) {
+ throw new IllegalArgumentException("Too many bind arguments. "
+ + bindArgs.length + " arguments were provided but the statement needs "
+ + mNumParameters + " arguments.");
}
+ System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length);
}
- nStatement = mCompiledSql.nStatement;
}
- @Override
- protected void onAllReferencesReleased() {
- release();
- mDatabase.removeSQLiteClosable(this);
- mDatabase.releaseReference();
+ final SQLiteDatabase getDatabase() {
+ return mDatabase;
}
- @Override
- protected void onAllReferencesReleasedFromContainer() {
- release();
- mDatabase.releaseReference();
+ final String getSql() {
+ return mSql;
}
- /* package */ void release() {
- if (mCompiledSql == null) {
- return;
- }
- mDatabase.releaseCompiledSqlObj(mSql, mCompiledSql);
- mCompiledSql = null;
- nStatement = 0;
+ final Object[] getBindArgs() {
+ return mBindArgs;
}
- /**
- * 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 -1;
+ final String[] getColumnNames() {
+ return mColumnNames;
}
- /**
- * used only for testing purposes
- */
- /* package */ int getSqlStatementId() {
- synchronized(this) {
- return (mCompiledSql == null) ? 0 : nStatement;
- }
+ /** @hide */
+ protected final SQLiteSession getSession() {
+ return mDatabase.getThreadSession();
}
- /* package */ String getSqlString() {
- return mSql;
+ /** @hide */
+ protected final int getConnectionFlags() {
+ return mDatabase.getThreadDefaultConnectionFlags(mReadOnly);
+ }
+
+ /** @hide */
+ protected final void onCorruption() {
+ mDatabase.onCorruption();
}
/**
+ * Unimplemented.
* @deprecated This method is deprecated and must not be used.
- *
- * @param sql the SQL string to compile
- * @param forceCompilation forces the SQL to be recompiled in the event that there is an
- * existing compiled SQL program already around
*/
@Deprecated
- protected void compile(String sql, boolean forceCompilation) {
- // TODO is there a need for this?
- }
-
- private void bind(int type, int index, Object value) {
- mDatabase.verifyDbIsOpen();
- addToBindArgs(index, (type == Cursor.FIELD_TYPE_NULL) ? null : value);
- if (nStatement > 0) {
- // bind only if the SQL statement is compiled
- acquireReference();
- try {
- switch (type) {
- case Cursor.FIELD_TYPE_NULL:
- native_bind_null(index);
- break;
- case Cursor.FIELD_TYPE_BLOB:
- native_bind_blob(index, (byte[]) value);
- break;
- case Cursor.FIELD_TYPE_FLOAT:
- native_bind_double(index, (Double) value);
- break;
- case Cursor.FIELD_TYPE_INTEGER:
- native_bind_long(index, (Long) value);
- break;
- case Cursor.FIELD_TYPE_STRING:
- default:
- native_bind_string(index, (String) value);
- break;
- }
- } finally {
- releaseReference();
- }
- }
+ public final int getUniqueId() {
+ return -1;
}
/**
@@ -260,7 +127,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param index The 1-based index to the parameter to bind null to
*/
public void bindNull(int index) {
- bind(Cursor.FIELD_TYPE_NULL, index, null);
+ bind(index, null);
}
/**
@@ -271,7 +138,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param value The value to bind
*/
public void bindLong(int index, long value) {
- bind(Cursor.FIELD_TYPE_INTEGER, index, value);
+ bind(index, value);
}
/**
@@ -282,7 +149,7 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* @param value The value to bind
*/
public void bindDouble(int index, double value) {
- bind(Cursor.FIELD_TYPE_FLOAT, index, value);
+ bind(index, value);
}
/**
@@ -290,13 +157,13 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* {@link #clearBindings} is called.
*
* @param index The 1-based index to the parameter to bind
- * @param value The value to bind
+ * @param value The value to bind, must not be null
*/
public void bindString(int index, String value) {
if (value == null) {
throw new IllegalArgumentException("the bind value at index " + index + " is null");
}
- bind(Cursor.FIELD_TYPE_STRING, index, value);
+ bind(index, value);
}
/**
@@ -304,29 +171,21 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* {@link #clearBindings} is called.
*
* @param index The 1-based index to the parameter to bind
- * @param value The value to bind
+ * @param value The value to bind, must not be null
*/
public void bindBlob(int index, byte[] value) {
if (value == null) {
throw new IllegalArgumentException("the bind value at index " + index + " is null");
}
- bind(Cursor.FIELD_TYPE_BLOB, index, value);
+ bind(index, value);
}
/**
* Clears all existing bindings. Unset bindings are treated as NULL.
*/
public void clearBindings() {
- mBindArgs = null;
- if (this.nStatement == 0) {
- return;
- }
- mDatabase.verifyDbIsOpen();
- acquireReference();
- try {
- native_clear_bindings();
- } finally {
- releaseReference();
+ if (mBindArgs != null) {
+ Arrays.fill(mBindArgs, null);
}
}
@@ -334,99 +193,33 @@ public abstract class SQLiteProgram extends SQLiteClosable {
* Release this program's resources, making it invalid.
*/
public void close() {
- mBindArgs = null;
- if (nHandle == 0 || !mDatabase.isOpen()) {
- return;
- }
releaseReference();
}
- private void addToBindArgs(int index, Object value) {
- if (mBindArgs == null) {
- mBindArgs = new HashMap<Integer, Object>();
- }
- mBindArgs.put(index, value);
- }
-
- /* package */ void compileAndbindAllArgs() {
- if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) {
- if (mBindArgs != null) {
- throw new IllegalArgumentException("Can't pass bindargs for this sql :" + mSql);
- }
- // no need to prepare this SQL statement
- return;
- }
- if (nStatement == 0) {
- // SQL statement is not compiled yet. compile it now.
- compileSql();
- }
- if (mBindArgs == null) {
- return;
- }
- for (int index : mBindArgs.keySet()) {
- Object value = mBindArgs.get(index);
- if (value == null) {
- native_bind_null(index);
- } else if (value instanceof Double || value instanceof Float) {
- native_bind_double(index, ((Number) value).doubleValue());
- } else if (value instanceof Number) {
- native_bind_long(index, ((Number) value).longValue());
- } else if (value instanceof Boolean) {
- Boolean bool = (Boolean)value;
- native_bind_long(index, (bool) ? 1 : 0);
- if (bool) {
- native_bind_long(index, 1);
- } else {
- native_bind_long(index, 0);
- }
- } else if (value instanceof byte[]){
- native_bind_blob(index, (byte[]) value);
- } else {
- native_bind_string(index, value.toString());
- }
- }
- }
-
/**
* Given an array of String bindArgs, this method binds all of them in one single call.
*
- * @param bindArgs the String array of bind args.
+ * @param bindArgs the String array of bind args, none of which must be null.
*/
public void bindAllArgsAsStrings(String[] bindArgs) {
- if (bindArgs == null) {
- return;
- }
- int size = bindArgs.length;
- for (int i = 0; i < size; i++) {
- bindString(i + 1, bindArgs[i]);
+ if (bindArgs != null) {
+ for (int i = bindArgs.length; i != 0; i--) {
+ bindString(i, bindArgs[i - 1]);
+ }
}
}
- /* package */ synchronized final void setNativeHandle(int nHandle) {
- this.nHandle = nHandle;
+ @Override
+ protected void onAllReferencesReleased() {
+ clearBindings();
}
- /**
- * @deprecated This method is deprecated and must not be used.
- * Compiles SQL into a SQLite program.
- *
- * <P>The database lock must be held when calling this method.
- * @param sql The SQL to compile.
- */
- @Deprecated
- protected final native void native_compile(String sql);
-
- /**
- * @deprecated This method is deprecated and must not be used.
- */
- @Deprecated
- protected final native void native_finalize();
-
- protected final native void native_bind_null(int index);
- protected final native void native_bind_long(int index, long value);
- 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();
+ private void bind(int index, Object value) {
+ if (index < 1 || index > mNumParameters) {
+ throw new IllegalArgumentException("Cannot bind argument at index "
+ + index + " because the index is out of range. "
+ + "The statement has " + mNumParameters + " parameters.");
+ }
+ mBindArgs[index - 1] = value;
+ }
}
-
diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java
index faf6cba..df2e260 100644
--- a/core/java/android/database/sqlite/SQLiteQuery.java
+++ b/core/java/android/database/sqlite/SQLiteQuery.java
@@ -16,160 +16,69 @@
package android.database.sqlite;
+import android.content.CancelationSignal;
+import android.content.OperationCanceledException;
import android.database.CursorWindow;
-import android.os.SystemClock;
-import android.text.TextUtils;
import android.util.Log;
/**
- * A SQLite program that represents a query that reads the resulting rows into a CursorWindow.
- * This class is used by SQLiteCursor and isn't useful itself.
- *
- * SQLiteQuery is not internally synchronized so code using a SQLiteQuery from multiple
- * threads should perform its own synchronization when using the SQLiteQuery.
+ * Represents a query that reads the resulting rows into a {@link SQLiteQuery}.
+ * This class is used by {@link SQLiteCursor} and isn't useful itself.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
*/
-public class SQLiteQuery extends SQLiteProgram {
+public final class SQLiteQuery extends SQLiteProgram {
private static final String TAG = "SQLiteQuery";
- private static native int nativeFillWindow(int databasePtr, int statementPtr, int windowPtr,
- int startPos, int offsetParam);
-
- private static native int nativeColumnCount(int statementPtr);
- private static native String nativeColumnName(int statementPtr, int columnIndex);
-
- /** The index of the unbound OFFSET parameter */
- private int mOffsetIndex = 0;
+ private final CancelationSignal mCancelationSignal;
- private boolean mClosed = false;
-
- /**
- * Create a persistent query object.
- *
- * @param db The database that this query object is associated with
- * @param query The SQL string for this query.
- * @param offsetIndex The 1-based index to the OFFSET parameter,
- */
- /* package */ SQLiteQuery(SQLiteDatabase db, String query, int offsetIndex, String[] bindArgs) {
- super(db, query);
- mOffsetIndex = offsetIndex;
- bindAllArgsAsStrings(bindArgs);
- }
+ SQLiteQuery(SQLiteDatabase db, String query, CancelationSignal cancelationSignal) {
+ super(db, query, null, cancelationSignal);
- /**
- * Constructor used to create new instance to replace a given instance of this class.
- * This constructor is used when the current Query object is now associated with a different
- * {@link SQLiteDatabase} object.
- *
- * @param db The database that this query object is associated with
- * @param query the instance of {@link SQLiteQuery} to be replaced
- */
- /* package */ SQLiteQuery(SQLiteDatabase db, SQLiteQuery query) {
- super(db, query.mSql);
- this.mBindArgs = query.mBindArgs;
- this.mOffsetIndex = query.mOffsetIndex;
+ mCancelationSignal = cancelationSignal;
}
/**
- * Reads rows into a buffer. This method acquires the database lock.
+ * Reads rows into a buffer.
*
* @param window The window to fill into
- * @return number of total rows in the query
+ * @param startPos The start position for filling the window.
+ * @param requiredPos The position of a row that MUST be in the window.
+ * If it won't fit, then the query should discard part of what it filled.
+ * @param countAllRows True to count all rows that the query would
+ * return regardless of whether they fit in the window.
+ * @return Number of rows that were enumerated. Might not be all rows
+ * unless countAllRows is true.
+ *
+ * @throws SQLiteException if an error occurs.
+ * @throws OperationCanceledException if the operation was canceled.
*/
- /* package */ int fillWindow(CursorWindow window) {
- mDatabase.lock(mSql);
- long timeStart = SystemClock.uptimeMillis();
+ int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) {
+ acquireReference();
try {
- acquireReference();
+ window.acquireReference();
try {
- window.acquireReference();
- int startPos = window.getStartPosition();
- int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr,
- startPos, mOffsetIndex);
- if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) {
- long elapsed = SystemClock.uptimeMillis() - timeStart;
- if (SQLiteDebug.shouldLogSlowQuery(elapsed)) {
- Log.d(TAG, "fillWindow took " + elapsed
- + " ms: window=\"" + window
- + "\", startPos=" + startPos
- + ", offset=" + mOffsetIndex
- + ", filledRows=" + window.getNumRows()
- + ", countedRows=" + numRows
- + ", query=\"" + mSql + "\""
- + ", args=[" + (mBindArgs != null ?
- TextUtils.join(", ", mBindArgs.values()) : "")
- + "]");
- }
- }
- mDatabase.logTimeStat(mSql, timeStart);
+ int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(),
+ window, startPos, requiredPos, countAllRows, getConnectionFlags(),
+ mCancelationSignal);
return numRows;
- } catch (IllegalStateException e){
- // simply ignore it
- return 0;
- } catch (SQLiteDatabaseCorruptException e) {
- mDatabase.onCorruption();
- throw e;
- } catch (SQLiteException e) {
- Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql);
- throw e;
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
+ } catch (SQLiteException ex) {
+ Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql());
+ throw ex;
} finally {
window.releaseReference();
}
} finally {
releaseReference();
- mDatabase.unlock();
- }
- }
-
- /**
- * Get the column count for the statement. Only valid on query based
- * statements. The database must be locked
- * when calling this method.
- *
- * @return The number of column in the statement's result set.
- */
- /* package */ int columnCountLocked() {
- acquireReference();
- try {
- return nativeColumnCount(nStatement);
- } finally {
- releaseReference();
- }
- }
-
- /**
- * Retrieves the column name for the given column index. The database must be locked
- * when calling this method.
- *
- * @param columnIndex the index of the column to get the name for
- * @return The requested column's name
- */
- /* package */ String columnNameLocked(int columnIndex) {
- acquireReference();
- try {
- return nativeColumnName(nStatement, columnIndex);
- } finally {
- releaseReference();
}
}
@Override
public String toString() {
- return "SQLiteQuery: " + mSql;
- }
-
- @Override
- public void close() {
- super.close();
- mClosed = true;
- }
-
- /**
- * Called by SQLiteCursor when it is requeried.
- */
- /* package */ void requery() {
- if (mClosed) {
- throw new IllegalStateException("requerying a closed cursor");
- }
- compileAndbindAllArgs();
+ return "SQLiteQuery: " + getSql();
}
}
diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
index 8f8eb6e..89469cb 100644
--- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java
+++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -16,6 +16,8 @@
package android.database.sqlite;
+import android.content.CancelationSignal;
+import android.content.OperationCanceledException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.provider.BaseColumns;
@@ -137,8 +139,9 @@ public class SQLiteQueryBuilder
/**
* Sets the cursor factory to be used for the query. You can use
* one factory for all queries on a database but it is normally
- * easier to specify the factory when doing this query. @param
- * factory the factor to use
+ * easier to specify the factory when doing this query.
+ *
+ * @param factory the factory to use.
*/
public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
mFactory = factory;
@@ -289,7 +292,7 @@ public class SQLiteQueryBuilder
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder) {
return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
- null /* limit */);
+ null /* limit */, null /* cancelationSignal */);
}
/**
@@ -327,6 +330,48 @@ public class SQLiteQueryBuilder
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit) {
+ return query(db, projectionIn, selection, selectionArgs,
+ groupBy, having, sortOrder, limit, null);
+ }
+
+ /**
+ * Perform a query by combining all current settings and the
+ * information passed into this method.
+ *
+ * @param db the database to query on
+ * @param projectionIn A list of which columns to return. Passing
+ * null will return all columns, which is discouraged to prevent
+ * reading data from storage that isn't going to be used.
+ * @param selection A filter declaring which rows to return,
+ * formatted as an SQL WHERE clause (excluding the WHERE
+ * itself). Passing null will return all rows for the given URL.
+ * @param selectionArgs You may include ?s in selection, which
+ * will be replaced by the values from selectionArgs, in order
+ * that they appear in the selection. The values will be bound
+ * as Strings.
+ * @param groupBy A filter declaring how to group rows, formatted
+ * as an SQL GROUP BY clause (excluding the GROUP BY
+ * itself). Passing null will cause the rows to not be grouped.
+ * @param having A filter declare which row groups to include in
+ * the cursor, if row grouping is being used, formatted as an
+ * SQL HAVING clause (excluding the HAVING itself). Passing
+ * null will cause all row groups to be included, and is
+ * required when row grouping is not being used.
+ * @param sortOrder How to order the rows, formatted as an SQL
+ * ORDER BY clause (excluding the ORDER BY itself). Passing null
+ * will use the default sort order, which may be unordered.
+ * @param limit Limits the number of rows returned by the query,
+ * formatted as LIMIT clause. Passing null denotes no LIMIT clause.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return a cursor over the result set
+ * @see android.content.ContentResolver#query(android.net.Uri, String[],
+ * String, String[], String)
+ */
+ public Cursor query(SQLiteDatabase db, String[] projectionIn,
+ String selection, String[] selectionArgs, String groupBy,
+ String having, String sortOrder, String limit, CancelationSignal cancelationSignal) {
if (mTables == null) {
return null;
}
@@ -341,7 +386,8 @@ public class SQLiteQueryBuilder
// in both the wrapped and original forms.
String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy,
having, sortOrder, limit);
- validateSql(db, sqlForValidation); // will throw if query is invalid
+ validateQuerySql(db, sqlForValidation,
+ cancelationSignal); // will throw if query is invalid
}
String sql = buildQuery(
@@ -353,20 +399,18 @@ public class SQLiteQueryBuilder
}
return db.rawQueryWithFactory(
mFactory, sql, selectionArgs,
- SQLiteDatabase.findEditTable(mTables)); // will throw if query is invalid
+ SQLiteDatabase.findEditTable(mTables),
+ cancelationSignal); // will throw if query is invalid
}
/**
- * Verifies that a SQL statement is valid by compiling it.
+ * Verifies that a SQL SELECT statement is valid by compiling it.
* If the SQL statement is not valid, this method will throw a {@link SQLiteException}.
*/
- private void validateSql(SQLiteDatabase db, String sql) {
- db.lock(sql);
- try {
- new SQLiteCompiledSql(db, sql).releaseSqlStatement();
- } finally {
- db.unlock();
- }
+ private void validateQuerySql(SQLiteDatabase db, String sql,
+ CancelationSignal cancelationSignal) {
+ db.getThreadSession().prepare(sql,
+ db.getThreadDefaultConnectionFlags(true /*readOnly*/), cancelationSignal, null);
}
/**
diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java
new file mode 100644
index 0000000..b5a3e31
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteSession.java
@@ -0,0 +1,963 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.content.CancelationSignal;
+import android.content.OperationCanceledException;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Provides a single client the ability to use a database.
+ *
+ * <h2>About database sessions</h2>
+ * <p>
+ * Database access is always performed using a session. The session
+ * manages the lifecycle of transactions and database connections.
+ * </p><p>
+ * Sessions can be used to perform both read-only and read-write operations.
+ * There is some advantage to knowing when a session is being used for
+ * read-only purposes because the connection pool can optimize the use
+ * of the available connections to permit multiple read-only operations
+ * to execute in parallel whereas read-write operations may need to be serialized.
+ * </p><p>
+ * When <em>Write Ahead Logging (WAL)</em> is enabled, the database can
+ * execute simultaneous read-only and read-write transactions, provided that
+ * at most one read-write transaction is performed at a time. When WAL is not
+ * enabled, read-only transactions can execute in parallel but read-write
+ * transactions are mutually exclusive.
+ * </p>
+ *
+ * <h2>Ownership and concurrency guarantees</h2>
+ * <p>
+ * Session objects are not thread-safe. In fact, session objects are thread-bound.
+ * The {@link SQLiteDatabase} uses a thread-local variable to associate a session
+ * with each thread for the use of that thread alone. Consequently, each thread
+ * has its own session object and therefore its own transaction state independent
+ * of other threads.
+ * </p><p>
+ * A thread has at most one session per database. This constraint ensures that
+ * a thread can never use more than one database connection at a time for a
+ * given database. As the number of available database connections is limited,
+ * if a single thread tried to acquire multiple connections for the same database
+ * at the same time, it might deadlock. Therefore we allow there to be only
+ * one session (so, at most one connection) per thread per database.
+ * </p>
+ *
+ * <h2>Transactions</h2>
+ * <p>
+ * There are two kinds of transaction: implicit transactions and explicit
+ * transactions.
+ * </p><p>
+ * An implicit transaction is created whenever a database operation is requested
+ * and there is no explicit transaction currently in progress. An implicit transaction
+ * only lasts for the duration of the database operation in question and then it
+ * is ended. If the database operation was successful, then its changes are committed.
+ * </p><p>
+ * An explicit transaction is started by calling {@link #beginTransaction} and
+ * specifying the desired transaction mode. Once an explicit transaction has begun,
+ * all subsequent database operations will be performed as part of that transaction.
+ * To end an explicit transaction, first call {@link #setTransactionSuccessful} if the
+ * transaction was successful, then call {@link #end}. If the transaction was
+ * marked successful, its changes will be committed, otherwise they will be rolled back.
+ * </p><p>
+ * Explicit transactions can also be nested. A nested explicit transaction is
+ * started with {@link #beginTransaction}, marked successful with
+ * {@link #setTransactionSuccessful}and ended with {@link #endTransaction}.
+ * If any nested transaction is not marked successful, then the entire transaction
+ * including all of its nested transactions will be rolled back
+ * when the outermost transaction is ended.
+ * </p><p>
+ * To improve concurrency, an explicit transaction can be yielded by calling
+ * {@link #yieldTransaction}. If there is contention for use of the database,
+ * then yielding ends the current transaction, commits its changes, releases the
+ * database connection for use by another session for a little while, and starts a
+ * new transaction with the same properties as the original one.
+ * Changes committed by {@link #yieldTransaction} cannot be rolled back.
+ * </p><p>
+ * When a transaction is started, the client can provide a {@link SQLiteTransactionListener}
+ * to listen for notifications of transaction-related events.
+ * </p><p>
+ * Recommended usage:
+ * <code><pre>
+ * // First, begin the transaction.
+ * session.beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, 0);
+ * try {
+ * // Then do stuff...
+ * session.execute("INSERT INTO ...", null, 0);
+ *
+ * // As the very last step before ending the transaction, mark it successful.
+ * session.setTransactionSuccessful();
+ * } finally {
+ * // Finally, end the transaction.
+ * // This statement will commit the transaction if it was marked successful or
+ * // roll it back otherwise.
+ * session.endTransaction();
+ * }
+ * </pre></code>
+ * </p>
+ *
+ * <h2>Database connections</h2>
+ * <p>
+ * A {@link SQLiteDatabase} can have multiple active sessions at the same
+ * time. Each session acquires and releases connections to the database
+ * as needed to perform each requested database transaction. If all connections
+ * are in use, then database transactions on some sessions will block until a
+ * connection becomes available.
+ * </p><p>
+ * The session acquires a single database connection only for the duration
+ * of a single (implicit or explicit) database transaction, then releases it.
+ * This characteristic allows a small pool of database connections to be shared
+ * efficiently by multiple sessions as long as they are not all trying to perform
+ * database transactions at the same time.
+ * </p>
+ *
+ * <h2>Responsiveness</h2>
+ * <p>
+ * Because there are a limited number of database connections and the session holds
+ * a database connection for the entire duration of a database transaction,
+ * it is important to keep transactions short. This is especially important
+ * for read-write transactions since they may block other transactions
+ * from executing. Consider calling {@link #yieldTransaction} periodically
+ * during long-running transactions.
+ * </p><p>
+ * Another important consideration is that transactions that take too long to
+ * run may cause the application UI to become unresponsive. Even if the transaction
+ * is executed in a background thread, the user will get bored and
+ * frustrated if the application shows no data for several seconds while
+ * a transaction runs.
+ * </p><p>
+ * Guidelines:
+ * <ul>
+ * <li>Do not perform database transactions on the UI thread.</li>
+ * <li>Keep database transactions as short as possible.</li>
+ * <li>Simple queries often run faster than complex queries.</li>
+ * <li>Measure the performance of your database transactions.</li>
+ * <li>Consider what will happen when the size of the data set grows.
+ * A query that works well on 100 rows may struggle with 10,000.</li>
+ * </ul>
+ *
+ * <h2>Reentrance</h2>
+ * <p>
+ * This class must tolerate reentrant execution of SQLite operations because
+ * triggers may call custom SQLite functions that perform additional queries.
+ * </p>
+ *
+ * @hide
+ */
+public final class SQLiteSession {
+ private final SQLiteConnectionPool mConnectionPool;
+
+ private SQLiteConnection mConnection;
+ private int mConnectionFlags;
+ private int mConnectionUseCount;
+ private Transaction mTransactionPool;
+ private Transaction mTransactionStack;
+
+ /**
+ * Transaction mode: Deferred.
+ * <p>
+ * In a deferred transaction, no locks are acquired on the database
+ * until the first operation is performed. If the first operation is
+ * read-only, then a <code>SHARED</code> lock is acquired, otherwise
+ * a <code>RESERVED</code> lock is acquired.
+ * </p><p>
+ * While holding a <code>SHARED</code> lock, this session is only allowed to
+ * read but other sessions are allowed to read or write.
+ * While holding a <code>RESERVED</code> lock, this session is allowed to read
+ * or write but other sessions are only allowed to read.
+ * </p><p>
+ * Because the lock is only acquired when needed in a deferred transaction,
+ * it is possible for another session to write to the database first before
+ * this session has a chance to do anything.
+ * </p><p>
+ * Corresponds to the SQLite <code>BEGIN DEFERRED</code> transaction mode.
+ * </p>
+ */
+ public static final int TRANSACTION_MODE_DEFERRED = 0;
+
+ /**
+ * Transaction mode: Immediate.
+ * <p>
+ * When an immediate transaction begins, the session acquires a
+ * <code>RESERVED</code> lock.
+ * </p><p>
+ * While holding a <code>RESERVED</code> lock, this session is allowed to read
+ * or write but other sessions are only allowed to read.
+ * </p><p>
+ * Corresponds to the SQLite <code>BEGIN IMMEDIATE</code> transaction mode.
+ * </p>
+ */
+ public static final int TRANSACTION_MODE_IMMEDIATE = 1;
+
+ /**
+ * Transaction mode: Exclusive.
+ * <p>
+ * When an exclusive transaction begins, the session acquires an
+ * <code>EXCLUSIVE</code> lock.
+ * </p><p>
+ * While holding an <code>EXCLUSIVE</code> lock, this session is allowed to read
+ * or write but no other sessions are allowed to access the database.
+ * </p><p>
+ * Corresponds to the SQLite <code>BEGIN EXCLUSIVE</code> transaction mode.
+ * </p>
+ */
+ public static final int TRANSACTION_MODE_EXCLUSIVE = 2;
+
+ /**
+ * Creates a session bound to the specified connection pool.
+ *
+ * @param connectionPool The connection pool.
+ */
+ public SQLiteSession(SQLiteConnectionPool connectionPool) {
+ if (connectionPool == null) {
+ throw new IllegalArgumentException("connectionPool must not be null");
+ }
+
+ mConnectionPool = connectionPool;
+ }
+
+ /**
+ * Returns true if the session has a transaction in progress.
+ *
+ * @return True if the session has a transaction in progress.
+ */
+ public boolean hasTransaction() {
+ return mTransactionStack != null;
+ }
+
+ /**
+ * Returns true if the session has a nested transaction in progress.
+ *
+ * @return True if the session has a nested transaction in progress.
+ */
+ public boolean hasNestedTransaction() {
+ return mTransactionStack != null && mTransactionStack.mParent != null;
+ }
+
+ /**
+ * Returns true if the session has an active database connection.
+ *
+ * @return True if the session has an active database connection.
+ */
+ public boolean hasConnection() {
+ return mConnection != null;
+ }
+
+ /**
+ * Begins a transaction.
+ * <p>
+ * Transactions may nest. If the transaction is not in progress,
+ * then a database connection is obtained and a new transaction is started.
+ * Otherwise, a nested transaction is started.
+ * </p><p>
+ * Each call to {@link #beginTransaction} must be matched exactly by a call
+ * to {@link #endTransaction}. To mark a transaction as successful,
+ * call {@link #setTransactionSuccessful} before calling {@link #endTransaction}.
+ * If the transaction is not successful, or if any of its nested
+ * transactions were not successful, then the entire transaction will
+ * be rolled back when the outermost transaction is ended.
+ * </p>
+ *
+ * @param transactionMode The transaction mode. One of: {@link #TRANSACTION_MODE_DEFERRED},
+ * {@link #TRANSACTION_MODE_IMMEDIATE}, or {@link #TRANSACTION_MODE_EXCLUSIVE}.
+ * Ignored when creating a nested transaction.
+ * @param transactionListener The transaction listener, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ *
+ * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been
+ * called for the current transaction.
+ * @throws SQLiteException if an error occurs.
+ * @throws OperationCanceledException if the operation was canceled.
+ *
+ * @see #setTransactionSuccessful
+ * @see #yieldTransaction
+ * @see #endTransaction
+ */
+ public void beginTransaction(int transactionMode,
+ SQLiteTransactionListener transactionListener, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ throwIfTransactionMarkedSuccessful();
+ beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags,
+ cancelationSignal);
+ }
+
+ private void beginTransactionUnchecked(int transactionMode,
+ SQLiteTransactionListener transactionListener, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ }
+
+ if (mTransactionStack == null) {
+ acquireConnection(null, connectionFlags, cancelationSignal); // might throw
+ }
+ try {
+ // Set up the transaction such that we can back out safely
+ // in case we fail part way.
+ if (mTransactionStack == null) {
+ // Execute SQL might throw a runtime exception.
+ switch (transactionMode) {
+ case TRANSACTION_MODE_IMMEDIATE:
+ mConnection.execute("BEGIN IMMEDIATE;", null,
+ cancelationSignal); // might throw
+ break;
+ case TRANSACTION_MODE_EXCLUSIVE:
+ mConnection.execute("BEGIN EXCLUSIVE;", null,
+ cancelationSignal); // might throw
+ break;
+ default:
+ mConnection.execute("BEGIN;", null, cancelationSignal); // might throw
+ break;
+ }
+ }
+
+ // Listener might throw a runtime exception.
+ if (transactionListener != null) {
+ try {
+ transactionListener.onBegin(); // might throw
+ } catch (RuntimeException ex) {
+ if (mTransactionStack == null) {
+ mConnection.execute("ROLLBACK;", null, cancelationSignal); // might throw
+ }
+ throw ex;
+ }
+ }
+
+ // Bookkeeping can't throw, except an OOM, which is just too bad...
+ Transaction transaction = obtainTransaction(transactionMode, transactionListener);
+ transaction.mParent = mTransactionStack;
+ mTransactionStack = transaction;
+ } finally {
+ if (mTransactionStack == null) {
+ releaseConnection(); // might throw
+ }
+ }
+ }
+
+ /**
+ * Marks the current transaction as having completed successfully.
+ * <p>
+ * This method can be called at most once between {@link #beginTransaction} and
+ * {@link #endTransaction} to indicate that the changes made by the transaction should be
+ * committed. If this method is not called, the changes will be rolled back
+ * when the transaction is ended.
+ * </p>
+ *
+ * @throws IllegalStateException if there is no current transaction, or if
+ * {@link #setTransactionSuccessful} has already been called for the current transaction.
+ *
+ * @see #beginTransaction
+ * @see #endTransaction
+ */
+ public void setTransactionSuccessful() {
+ throwIfNoTransaction();
+ throwIfTransactionMarkedSuccessful();
+
+ mTransactionStack.mMarkedSuccessful = true;
+ }
+
+ /**
+ * Ends the current transaction and commits or rolls back changes.
+ * <p>
+ * If this is the outermost transaction (not nested within any other
+ * transaction), then the changes are committed if {@link #setTransactionSuccessful}
+ * was called or rolled back otherwise.
+ * </p><p>
+ * This method must be called exactly once for each call to {@link #beginTransaction}.
+ * </p>
+ *
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ *
+ * @throws IllegalStateException if there is no current transaction.
+ * @throws SQLiteException if an error occurs.
+ * @throws OperationCanceledException if the operation was canceled.
+ *
+ * @see #beginTransaction
+ * @see #setTransactionSuccessful
+ * @see #yieldTransaction
+ */
+ public void endTransaction(CancelationSignal cancelationSignal) {
+ throwIfNoTransaction();
+ assert mConnection != null;
+
+ endTransactionUnchecked(cancelationSignal);
+ }
+
+ private void endTransactionUnchecked(CancelationSignal cancelationSignal) {
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ }
+
+ final Transaction top = mTransactionStack;
+ boolean successful = top.mMarkedSuccessful && !top.mChildFailed;
+
+ RuntimeException listenerException = null;
+ final SQLiteTransactionListener listener = top.mListener;
+ if (listener != null) {
+ try {
+ if (successful) {
+ listener.onCommit(); // might throw
+ } else {
+ listener.onRollback(); // might throw
+ }
+ } catch (RuntimeException ex) {
+ listenerException = ex;
+ successful = false;
+ }
+ }
+
+ mTransactionStack = top.mParent;
+ recycleTransaction(top);
+
+ if (mTransactionStack != null) {
+ if (!successful) {
+ mTransactionStack.mChildFailed = true;
+ }
+ } else {
+ try {
+ if (successful) {
+ mConnection.execute("COMMIT;", null, cancelationSignal); // might throw
+ } else {
+ mConnection.execute("ROLLBACK;", null, cancelationSignal); // might throw
+ }
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ if (listenerException != null) {
+ throw listenerException;
+ }
+ }
+
+ /**
+ * Temporarily ends a transaction to let other threads have use of
+ * the database. Begins a new transaction after a specified delay.
+ * <p>
+ * If there are other threads waiting to acquire connections,
+ * then the current transaction is committed and the database
+ * connection is released. After a short delay, a new transaction
+ * is started.
+ * </p><p>
+ * The transaction is assumed to be successful so far. Do not call
+ * {@link #setTransactionSuccessful()} before calling this method.
+ * This method will fail if the transaction has already been marked
+ * successful.
+ * </p><p>
+ * The changes that were committed by a yield cannot be rolled back later.
+ * </p><p>
+ * Before this method was called, there must already have been
+ * a transaction in progress. When this method returns, there will
+ * still be a transaction in progress, either the same one as before
+ * or a new one if the transaction was actually yielded.
+ * </p><p>
+ * This method should not be called when there is a nested transaction
+ * in progress because it is not possible to yield a nested transaction.
+ * If <code>throwIfNested</code> is true, then attempting to yield
+ * a nested transaction will throw {@link IllegalStateException}, otherwise
+ * the method will return <code>false</code> in that case.
+ * </p><p>
+ * If there is no nested transaction in progress but a previous nested
+ * transaction failed, then the transaction is not yielded (because it
+ * must be rolled back) and this method returns <code>false</code>.
+ * </p>
+ *
+ * @param sleepAfterYieldDelayMillis A delay time to wait after yielding
+ * the database connection to allow other threads some time to run.
+ * If the value is less than or equal to zero, there will be no additional
+ * delay beyond the time it will take to begin a new transaction.
+ * @param throwIfUnsafe If true, then instead of returning false when no
+ * transaction is in progress, a nested transaction is in progress, or when
+ * the transaction has already been marked successful, throws {@link IllegalStateException}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return True if the transaction was actually yielded.
+ *
+ * @throws IllegalStateException if <code>throwIfNested</code> is true and
+ * there is no current transaction, there is a nested transaction in progress or
+ * if {@link #setTransactionSuccessful} has already been called for the current transaction.
+ * @throws SQLiteException if an error occurs.
+ * @throws OperationCanceledException if the operation was canceled.
+ *
+ * @see #beginTransaction
+ * @see #endTransaction
+ */
+ public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe,
+ CancelationSignal cancelationSignal) {
+ if (throwIfUnsafe) {
+ throwIfNoTransaction();
+ throwIfTransactionMarkedSuccessful();
+ throwIfNestedTransaction();
+ } else {
+ if (mTransactionStack == null || mTransactionStack.mMarkedSuccessful
+ || mTransactionStack.mParent != null) {
+ return false;
+ }
+ }
+ assert mConnection != null;
+
+ if (mTransactionStack.mChildFailed) {
+ return false;
+ }
+
+ return yieldTransactionUnchecked(sleepAfterYieldDelayMillis,
+ cancelationSignal); // might throw
+ }
+
+ private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis,
+ CancelationSignal cancelationSignal) {
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ }
+
+ if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) {
+ return false;
+ }
+
+ final int transactionMode = mTransactionStack.mMode;
+ final SQLiteTransactionListener listener = mTransactionStack.mListener;
+ final int connectionFlags = mConnectionFlags;
+ endTransactionUnchecked(cancelationSignal); // might throw
+
+ if (sleepAfterYieldDelayMillis > 0) {
+ try {
+ Thread.sleep(sleepAfterYieldDelayMillis);
+ } catch (InterruptedException ex) {
+ // we have been interrupted, that's all we need to do
+ }
+ }
+
+ beginTransactionUnchecked(transactionMode, listener, connectionFlags,
+ cancelationSignal); // might throw
+ return true;
+ }
+
+ /**
+ * Prepares a statement for execution but does not bind its parameters or execute it.
+ * <p>
+ * This method can be used to check for syntax errors during compilation
+ * prior to execution of the statement. If the {@code outStatementInfo} argument
+ * is not null, the provided {@link SQLiteStatementInfo} object is populated
+ * with information about the statement.
+ * </p><p>
+ * A prepared statement makes no reference to the arguments that may eventually
+ * be bound to it, consequently it it possible to cache certain prepared statements
+ * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable,
+ * then it will be stored in the cache for later and reused if possible.
+ * </p>
+ *
+ * @param sql The SQL statement to prepare.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate
+ * with information about the statement, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public void prepare(String sql, int connectionFlags, CancelationSignal cancelationSignal,
+ SQLiteStatementInfo outStatementInfo) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ mConnection.prepare(sql, outStatementInfo); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that does not return a result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public void execute(String sql, Object[] bindArgs, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ return;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ mConnection.execute(sql, bindArgs, cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single <code>long</code> result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>long</code>, or zero if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public long executeForLong(String sql, Object[] bindArgs, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ return 0;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ return mConnection.executeForLong(sql, bindArgs, cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single {@link String} result.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The value of the first column in the first row of the result set
+ * as a <code>String</code>, or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public String executeForString(String sql, Object[] bindArgs, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ return null;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ return mConnection.executeForString(sql, bindArgs, cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a single BLOB result as a
+ * file descriptor to a shared memory region.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The file descriptor for a shared memory region that contains
+ * the value of the first column in the first row of the result set as a BLOB,
+ * or null if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs,
+ int connectionFlags, CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ return null;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ return mConnection.executeForBlobFileDescriptor(sql, bindArgs,
+ cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns a count of the number of rows
+ * that were changed. Use for UPDATE or DELETE SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The number of rows that were changed.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ return 0;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ return mConnection.executeForChangedRowCount(sql, bindArgs,
+ cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement that returns the row id of the last row inserted
+ * by the statement. Use for INSERT SQL statements.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The row id of the last row that was inserted, or 0 if none.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ return 0;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ return mConnection.executeForLastInsertedRowId(sql, bindArgs,
+ cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Executes a statement and populates the specified {@link CursorWindow}
+ * with a range of results. Returns the number of rows that were counted
+ * during query execution.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param window The cursor window to clear and fill.
+ * @param startPos The start position for filling the window.
+ * @param requiredPos The position of a row that MUST be in the window.
+ * If it won't fit, then the query should discard part of what it filled
+ * so that it does. Must be greater than or equal to <code>startPos</code>.
+ * @param countAllRows True to count all rows that the query would return
+ * regagless of whether they fit in the window.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return The number of rows that were counted during query execution. Might
+ * not be all rows in the result set unless <code>countAllRows</code> is true.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ public int executeForCursorWindow(String sql, Object[] bindArgs,
+ CursorWindow window, int startPos, int requiredPos, boolean countAllRows,
+ int connectionFlags, CancelationSignal cancelationSignal) {
+ if (sql == null) {
+ throw new IllegalArgumentException("sql must not be null.");
+ }
+ if (window == null) {
+ throw new IllegalArgumentException("window must not be null.");
+ }
+
+ if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) {
+ window.clear();
+ return 0;
+ }
+
+ acquireConnection(sql, connectionFlags, cancelationSignal); // might throw
+ try {
+ return mConnection.executeForCursorWindow(sql, bindArgs,
+ window, startPos, requiredPos, countAllRows,
+ cancelationSignal); // might throw
+ } finally {
+ releaseConnection(); // might throw
+ }
+ }
+
+ /**
+ * Performs special reinterpretation of certain SQL statements such as "BEGIN",
+ * "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are
+ * maintained.
+ *
+ * This function is mainly used to support legacy apps that perform their
+ * own transactions by executing raw SQL rather than calling {@link #beginTransaction}
+ * and the like.
+ *
+ * @param sql The SQL statement to execute.
+ * @param bindArgs The arguments to bind, or null if none.
+ * @param connectionFlags The connection flags to use if a connection must be
+ * acquired by this operation. Refer to {@link SQLiteConnectionPool}.
+ * @param cancelationSignal A signal to cancel the operation in progress, or null if none.
+ * @return True if the statement was of a special form that was handled here,
+ * false otherwise.
+ *
+ * @throws SQLiteException if an error occurs, such as a syntax error
+ * or invalid number of bind arguments.
+ * @throws OperationCanceledException if the operation was canceled.
+ */
+ private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (cancelationSignal != null) {
+ cancelationSignal.throwIfCanceled();
+ }
+
+ final int type = DatabaseUtils.getSqlStatementType(sql);
+ switch (type) {
+ case DatabaseUtils.STATEMENT_BEGIN:
+ beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags,
+ cancelationSignal);
+ return true;
+
+ case DatabaseUtils.STATEMENT_COMMIT:
+ setTransactionSuccessful();
+ endTransaction(cancelationSignal);
+ return true;
+
+ case DatabaseUtils.STATEMENT_ABORT:
+ endTransaction(cancelationSignal);
+ return true;
+ }
+ return false;
+ }
+
+ private void acquireConnection(String sql, int connectionFlags,
+ CancelationSignal cancelationSignal) {
+ if (mConnection == null) {
+ assert mConnectionUseCount == 0;
+ mConnection = mConnectionPool.acquireConnection(sql, connectionFlags,
+ cancelationSignal); // might throw
+ mConnectionFlags = connectionFlags;
+ }
+ mConnectionUseCount += 1;
+ }
+
+ private void releaseConnection() {
+ assert mConnection != null;
+ assert mConnectionUseCount > 0;
+ if (--mConnectionUseCount == 0) {
+ try {
+ mConnectionPool.releaseConnection(mConnection); // might throw
+ } finally {
+ mConnection = null;
+ }
+ }
+ }
+
+ private void throwIfNoTransaction() {
+ if (mTransactionStack == null) {
+ throw new IllegalStateException("Cannot perform this operation because "
+ + "there is no current transaction.");
+ }
+ }
+
+ private void throwIfTransactionMarkedSuccessful() {
+ if (mTransactionStack != null && mTransactionStack.mMarkedSuccessful) {
+ throw new IllegalStateException("Cannot perform this operation because "
+ + "the transaction has already been marked successful. The only "
+ + "thing you can do now is call endTransaction().");
+ }
+ }
+
+ private void throwIfNestedTransaction() {
+ if (mTransactionStack == null && mTransactionStack.mParent != null) {
+ throw new IllegalStateException("Cannot perform this operation because "
+ + "a nested transaction is in progress.");
+ }
+ }
+
+ private Transaction obtainTransaction(int mode, SQLiteTransactionListener listener) {
+ Transaction transaction = mTransactionPool;
+ if (transaction != null) {
+ mTransactionPool = transaction.mParent;
+ transaction.mParent = null;
+ transaction.mMarkedSuccessful = false;
+ transaction.mChildFailed = false;
+ } else {
+ transaction = new Transaction();
+ }
+ transaction.mMode = mode;
+ transaction.mListener = listener;
+ return transaction;
+ }
+
+ private void recycleTransaction(Transaction transaction) {
+ transaction.mParent = mTransactionPool;
+ transaction.mListener = null;
+ mTransactionPool = transaction;
+ }
+
+ private static final class Transaction {
+ public Transaction mParent;
+ public int mMode;
+ public SQLiteTransactionListener mListener;
+ public boolean mMarkedSuccessful;
+ public boolean mChildFailed;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
index ff973a7..b1092d7 100644
--- a/core/java/android/database/sqlite/SQLiteStatement.java
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -16,47 +16,19 @@
package android.database.sqlite;
-import android.database.DatabaseUtils;
import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.io.IOException;
-
-import dalvik.system.BlockGuard;
/**
- * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused.
- * 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.
+ * Represents a statement that can be executed against a database. The statement
+ * cannot return multiple rows or columns, but single value (1 x 1) result sets
+ * are supported.
+ * <p>
+ * This class is not thread-safe.
+ * </p>
*/
-@SuppressWarnings("deprecation")
-public class SQLiteStatement extends SQLiteProgram
-{
- private static final String TAG = "SQLiteStatement";
-
- private static final boolean READ = true;
- private static final boolean WRITE = false;
-
- private SQLiteDatabase mOrigDb;
- private int mState;
- /** possible value for {@link #mState}. indicates that a transaction is started. */
- private static final int TRANS_STARTED = 1;
- /** possible value for {@link #mState}. indicates that a lock is acquired. */
- private static final int LOCK_ACQUIRED = 2;
-
- /**
- * Don't use SQLiteStatement constructor directly, please use
- * {@link SQLiteDatabase#compileStatement(String)}
- * @param db
- * @param sql
- */
- /* package */ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
- super(db, sql, bindArgs, false /* don't compile sql statement */);
+public final class SQLiteStatement extends SQLiteProgram {
+ SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
+ super(db, sql, bindArgs, null);
}
/**
@@ -67,7 +39,15 @@ public class SQLiteStatement extends SQLiteProgram
* some reason
*/
public void execute() {
- executeUpdateDelete();
+ acquireReference();
+ try {
+ getSession().execute(getSql(), getBindArgs(), getConnectionFlags(), null);
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
+ } finally {
+ releaseReference();
+ }
}
/**
@@ -79,21 +59,15 @@ public class SQLiteStatement extends SQLiteProgram
* some reason
*/
public int executeUpdateDelete() {
+ acquireReference();
try {
- saveSqlAsLastSqlStatement();
- acquireAndLock(WRITE);
- int numChanges = 0;
- if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) {
- // since the statement doesn't have to be prepared,
- // call the following native method which will not prepare
- // the query plan
- native_executeSql(mSql);
- } else {
- numChanges = native_execute();
- }
- return numChanges;
+ return getSession().executeForChangedRowCount(
+ getSql(), getBindArgs(), getConnectionFlags(), null);
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
@@ -107,23 +81,18 @@ public class SQLiteStatement extends SQLiteProgram
* some reason
*/
public long executeInsert() {
+ acquireReference();
try {
- saveSqlAsLastSqlStatement();
- acquireAndLock(WRITE);
- return native_executeInsert();
+ return getSession().executeForLastInsertedRowId(
+ getSql(), getBindArgs(), getConnectionFlags(), null);
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
- private void saveSqlAsLastSqlStatement() {
- if (((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_UPDATE) ||
- (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_BEGIN) {
- mDatabase.setLastSqlStatement(mSql);
- }
- }
/**
* Execute a statement that returns a 1 by 1 table with a numeric value.
* For example, SELECT COUNT(*) FROM table;
@@ -133,17 +102,15 @@ public class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public long simpleQueryForLong() {
+ acquireReference();
try {
- long timeStart = acquireAndLock(READ);
- long retValue = native_1x1_long();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } catch (SQLiteDoneException e) {
- throw new SQLiteDoneException(
- "expected 1 row from this query but query returned no data. check the query: " +
- mSql);
+ return getSession().executeForLong(
+ getSql(), getBindArgs(), getConnectionFlags(), null);
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
@@ -156,17 +123,15 @@ public class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public String simpleQueryForString() {
+ acquireReference();
try {
- long timeStart = acquireAndLock(READ);
- String retValue = native_1x1_string();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } catch (SQLiteDoneException e) {
- throw new SQLiteDoneException(
- "expected 1 row from this query but query returned no data. check the query: " +
- mSql);
+ return getSession().executeForString(
+ getSql(), getBindArgs(), getConnectionFlags(), null);
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
+ releaseReference();
}
}
@@ -179,121 +144,20 @@ public class SQLiteStatement extends SQLiteProgram
* @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
*/
public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() {
+ acquireReference();
try {
- long timeStart = acquireAndLock(READ);
- ParcelFileDescriptor retValue = native_1x1_blob_ashmem();
- mDatabase.logTimeStat(mSql, timeStart);
- return retValue;
- } catch (IOException ex) {
- Log.e(TAG, "simpleQueryForBlobFileDescriptor() failed", ex);
- return null;
- } catch (SQLiteDoneException e) {
- throw new SQLiteDoneException(
- "expected 1 row from this query but query returned no data. check the query: " +
- mSql);
+ return getSession().executeForBlobFileDescriptor(
+ getSql(), getBindArgs(), getConnectionFlags(), null);
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ throw ex;
} finally {
- releaseAndUnlock();
- }
- }
-
- /**
- * 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>if the SQL statement is an update, start transaction if not already in one.
- * otherwise, get lock on the database</li>
- * <li>acquire reference on this object</li>
- * <li>and then return the current time _after_ the database lock was acquired</li>
- * </ul>
- * <p>
- * This method removes the duplicate code from the other public
- * methods in this class.
- */
- private long acquireAndLock(boolean rwFlag) {
- mState = 0;
- // use pooled database connection handles for SELECT SQL statements
- mDatabase.verifyDbIsOpen();
- SQLiteDatabase db = ((mStatementType & SQLiteProgram.STATEMENT_USE_POOLED_CONN) > 0)
- ? mDatabase.getDbConnection(mSql) : mDatabase;
- // use the database connection obtained above
- mOrigDb = mDatabase;
- mDatabase = db;
- setNativeHandle(mDatabase.mNativeHandle);
- if (rwFlag == WRITE) {
- BlockGuard.getThreadPolicy().onWriteToDisk();
- } else {
- BlockGuard.getThreadPolicy().onReadFromDisk();
- }
-
- /*
- * Special case handling of SQLiteDatabase.execSQL("BEGIN transaction").
- * we know it is execSQL("BEGIN transaction") from the caller IF there is no lock held.
- * beginTransaction() methods in SQLiteDatabase call lockForced() before
- * calling execSQL("BEGIN transaction").
- */
- if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == DatabaseUtils.STATEMENT_BEGIN) {
- if (!mDatabase.isDbLockedByCurrentThread()) {
- // transaction is NOT started by calling beginTransaction() methods in
- // SQLiteDatabase
- mDatabase.setTransactionUsingExecSqlFlag();
- }
- } else if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_UPDATE) {
- // got update SQL statement. if there is NO pending transaction, start one
- if (!mDatabase.inTransaction()) {
- mDatabase.beginTransactionNonExclusive();
- mState = TRANS_STARTED;
- }
+ releaseReference();
}
- // do I have database lock? if not, grab it.
- if (!mDatabase.isDbLockedByCurrentThread()) {
- mDatabase.lock(mSql);
- mState = LOCK_ACQUIRED;
- }
-
- acquireReference();
- long startTime = SystemClock.uptimeMillis();
- mDatabase.closePendingStatements();
- compileAndbindAllArgs();
- return startTime;
}
- /**
- * this method releases locks and references acquired in {@link #acquireAndLock(boolean)}
- */
- private void releaseAndUnlock() {
- releaseReference();
- if (mState == TRANS_STARTED) {
- try {
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- } else if (mState == LOCK_ACQUIRED) {
- mDatabase.unlock();
- }
- if ((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_COMMIT ||
- (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) ==
- DatabaseUtils.STATEMENT_ABORT) {
- mDatabase.resetTransactionUsingExecSqlFlag();
- }
- 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.
- release();
- // restore the database connection handle to the original value
- mDatabase = mOrigDb;
- setNativeHandle(mDatabase.mNativeHandle);
+ @Override
+ public String toString() {
+ return "SQLiteProgram: " + getSql();
}
-
- private final native int native_execute();
- private final native long native_executeInsert();
- private final native long native_1x1_long();
- private final native String native_1x1_string();
- private final native ParcelFileDescriptor native_1x1_blob_ashmem() throws IOException;
- private final native void native_executeSql(String sql);
}
diff --git a/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java b/core/java/android/database/sqlite/SQLiteStatementInfo.java
index bcf95e2..3edfdb0 100644
--- a/core/java/android/database/sqlite/SQLiteUnfinalizedObjectsException.java
+++ b/core/java/android/database/sqlite/SQLiteStatementInfo.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,23 @@
package android.database.sqlite;
/**
- * Thrown if the database can't be closed because of some un-closed
- * Cursor or SQLiteStatement objects. Could happen when a thread is trying to close
- * the database while another thread still hasn't closed a Cursor on that database.
+ * Describes a SQLite statement.
+ *
* @hide
*/
-public class SQLiteUnfinalizedObjectsException extends SQLiteException {
- public SQLiteUnfinalizedObjectsException() {}
+public final class SQLiteStatementInfo {
+ /**
+ * The number of parameters that the statement has.
+ */
+ public int numParameters;
+
+ /**
+ * The names of all columns in the result set of the statement.
+ */
+ public String[] columnNames;
- public SQLiteUnfinalizedObjectsException(String error) {
- super(error);
- }
+ /**
+ * True if the statement is read-only.
+ */
+ public boolean readOnly;
}
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 7ca6155..cca208a 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -138,7 +138,7 @@ public class Camera {
private static final int CAMERA_MSG_COMPRESSED_IMAGE = 0x100;
private static final int CAMERA_MSG_RAW_IMAGE_NOTIFY = 0x200;
private static final int CAMERA_MSG_PREVIEW_METADATA = 0x400;
- private static final int CAMERA_MSG_ALL_MSGS = 0x4FF;
+ private static final int CAMERA_MSG_FOCUS_MOVE = 0x800;
private int mNativeContext; // accessed by native methods
private EventHandler mEventHandler;
@@ -148,6 +148,7 @@ public class Camera {
private PreviewCallback mPreviewCallback;
private PictureCallback mPostviewCallback;
private AutoFocusCallback mAutoFocusCallback;
+ private AutoFocusMoveCallback mAutoFocusMoveCallback;
private OnZoomChangeListener mZoomListener;
private FaceDetectionListener mFaceListener;
private ErrorCallback mErrorCallback;
@@ -302,6 +303,12 @@ public class Camera {
native_setup(new WeakReference<Camera>(this), cameraId);
}
+ /**
+ * An empty Camera for testing purpose.
+ */
+ Camera() {
+ }
+
protected void finalize() {
release();
}
@@ -492,6 +499,7 @@ public class Camera {
mPostviewCallback = null;
mJpegCallback = null;
mAutoFocusCallback = null;
+ mAutoFocusMoveCallback = null;
}
private native final void _stopPreview();
@@ -737,6 +745,12 @@ public class Camera {
}
return;
+ case CAMERA_MSG_FOCUS_MOVE:
+ if (mAutoFocusMoveCallback != null) {
+ mAutoFocusMoveCallback.onAutoFocusMoving(msg.arg1 == 0 ? false : true, mCamera);
+ }
+ return;
+
default:
Log.e(TAG, "Unknown message type " + msg.what);
return;
@@ -849,6 +863,39 @@ public class Camera {
private native final void native_cancelAutoFocus();
/**
+ * Callback interface used to notify on auto focus start and stop.
+ *
+ * <p>This is useful for continuous autofocus -- {@link Parameters#FOCUS_MODE_CONTINUOUS_VIDEO}
+ * and {@link Parameters#FOCUS_MODE_CONTINUOUS_PICTURE}. Applications can
+ * show autofocus animation.</p>
+ *
+ * @hide
+ */
+ public interface AutoFocusMoveCallback
+ {
+ /**
+ * Called when the camera auto focus starts or stops.
+ *
+ * @param start true if focus starts to move, false if focus stops to move
+ * @param camera the Camera service object
+ */
+ void onAutoFocusMoving(boolean start, Camera camera);
+ }
+
+ /**
+ * Sets camera auto-focus move callback.
+ *
+ * @param cb the callback to run
+ * @hide
+ */
+ public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) {
+ mAutoFocusMoveCallback = cb;
+ enableFocusMoveCallback((mAutoFocusMoveCallback != null) ? 1 : 0);
+ }
+
+ private native void enableFocusMoveCallback(int enable);
+
+ /**
* Callback interface used to signal the moment of actual image capture.
*
* @see #takePicture(ShutterCallback, PictureCallback, PictureCallback, PictureCallback)
@@ -1310,6 +1357,18 @@ public class Camera {
}
/**
+ * Returns an empty {@link Parameters} for testing purpose.
+ *
+ * @return an Parameter object.
+ *
+ * @hide
+ */
+ public static Parameters getEmptyParameters() {
+ Camera camera = new Camera();
+ return camera.new Parameters();
+ }
+
+ /**
* Image size (width and height dimensions).
*/
public class Size {
diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java
index 10c1195..23ae21b 100644
--- a/core/java/android/inputmethodservice/ExtractEditText.java
+++ b/core/java/android/inputmethodservice/ExtractEditText.java
@@ -100,6 +100,9 @@ public class ExtractEditText extends EditText {
@Override public boolean onTextContextMenuItem(int id) {
if (mIME != null && mIME.onExtractTextContextMenuItem(id)) {
+ // Mode was started on Extracted, needs to be stopped here.
+ // Cut and paste will change the text, which stops selection mode.
+ if (id == android.R.id.copy) stopSelectionActionMode();
return true;
}
return super.onTextContextMenuItem(id);
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 0052dd0..a569317 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -360,6 +360,11 @@ public class ConnectivityManager {
}
}
+ /**
+ * Gets you info about the current data network.
+ * Call {@link NetworkInfo#isConnected()} on the returned {@link NetworkInfo}
+ * to check if the device has a data connection.
+ */
public NetworkInfo getActiveNetworkInfo() {
try {
return mService.getActiveNetworkInfo();
diff --git a/core/java/android/net/DhcpStateMachine.java b/core/java/android/net/DhcpStateMachine.java
index fc6a44a..397a12a 100644
--- a/core/java/android/net/DhcpStateMachine.java
+++ b/core/java/android/net/DhcpStateMachine.java
@@ -347,21 +347,25 @@ public class DhcpStateMachine extends StateMachine {
if (success) {
if (DBG) Log.d(TAG, "DHCP succeeded on " + mInterfaceName);
- long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion
-
- //Sanity check for renewal
- //TODO: would be good to notify the user that his network configuration is
- //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS
- if (leaseDuration < MIN_RENEWAL_TIME_SECS) {
- leaseDuration = MIN_RENEWAL_TIME_SECS;
- }
- //Do it a bit earlier than half the lease duration time
- //to beat the native DHCP client and avoid extra packets
- //48% for one hour lease time = 29 minutes
- mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime() +
- leaseDuration * 480, //in milliseconds
- mDhcpRenewalIntent);
+ long leaseDuration = dhcpInfoInternal.leaseDuration; //int to long conversion
+
+ //Sanity check for renewal
+ if (leaseDuration >= 0) {
+ //TODO: would be good to notify the user that his network configuration is
+ //bad and that the device cannot renew below MIN_RENEWAL_TIME_SECS
+ if (leaseDuration < MIN_RENEWAL_TIME_SECS) {
+ leaseDuration = MIN_RENEWAL_TIME_SECS;
+ }
+ //Do it a bit earlier than half the lease duration time
+ //to beat the native DHCP client and avoid extra packets
+ //48% for one hour lease time = 29 minutes
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() +
+ leaseDuration * 480, //in milliseconds
+ mDhcpRenewalIntent);
+ } else {
+ //infinite lease time, no renewal needed
+ }
mController.obtainMessage(CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, dhcpInfoInternal)
.sendToTarget();
diff --git a/core/java/android/net/InterfaceConfiguration.java b/core/java/android/net/InterfaceConfiguration.java
index 89b5915..8cdd153 100644
--- a/core/java/android/net/InterfaceConfiguration.java
+++ b/core/java/android/net/InterfaceConfiguration.java
@@ -16,34 +16,84 @@
package android.net;
-import android.os.Parcelable;
import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.android.collect.Sets;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
+import java.util.HashSet;
/**
- * A simple object for retrieving / setting an interfaces configuration
+ * Configuration details for a network interface.
+ *
* @hide
*/
public class InterfaceConfiguration implements Parcelable {
- public String hwAddr;
- public LinkAddress addr;
- public String interfaceFlags;
+ private String mHwAddr;
+ private LinkAddress mAddr;
+ private HashSet<String> mFlags = Sets.newHashSet();
- public InterfaceConfiguration() {
- super();
- }
+ private static final String FLAG_UP = "up";
+ private static final String FLAG_DOWN = "down";
+ @Override
public String toString() {
- StringBuffer str = new StringBuffer();
+ final StringBuilder builder = new StringBuilder();
+ builder.append("mHwAddr=").append(mHwAddr);
+ builder.append(" mAddr=").append(String.valueOf(mAddr));
+ builder.append(" mFlags=").append(getFlags());
+ return builder.toString();
+ }
+
+ public Iterable<String> getFlags() {
+ return mFlags;
+ }
+
+ public boolean hasFlag(String flag) {
+ validateFlag(flag);
+ return mFlags.contains(flag);
+ }
+
+ public void clearFlag(String flag) {
+ validateFlag(flag);
+ mFlags.remove(flag);
+ }
+
+ public void setFlag(String flag) {
+ validateFlag(flag);
+ mFlags.add(flag);
+ }
+
+ /**
+ * Set flags to mark interface as up.
+ */
+ public void setInterfaceUp() {
+ mFlags.remove(FLAG_DOWN);
+ mFlags.add(FLAG_UP);
+ }
- str.append("ipddress ");
- str.append((addr != null) ? addr.toString() : "NULL");
- str.append(" flags ").append(interfaceFlags);
- str.append(" hwaddr ").append(hwAddr);
+ /**
+ * Set flags to mark interface as down.
+ */
+ public void setInterfaceDown() {
+ mFlags.remove(FLAG_UP);
+ mFlags.add(FLAG_DOWN);
+ }
+
+ public LinkAddress getLinkAddress() {
+ return mAddr;
+ }
+
+ public void setLinkAddress(LinkAddress addr) {
+ mAddr = addr;
+ }
+
+ public String getHardwareAddress() {
+ return mHwAddr;
+ }
- return str.toString();
+ public void setHardwareAddress(String hwAddr) {
+ mHwAddr = hwAddr;
}
/**
@@ -55,8 +105,8 @@ public class InterfaceConfiguration implements Parcelable {
*/
public boolean isActive() {
try {
- if(interfaceFlags.contains("up")) {
- for (byte b : addr.getAddress().getAddress()) {
+ if (hasFlag(FLAG_UP)) {
+ for (byte b : mAddr.getAddress().getAddress()) {
if (b != 0) return true;
}
}
@@ -66,38 +116,49 @@ public class InterfaceConfiguration implements Parcelable {
return false;
}
- /** Implement the Parcelable interface {@hide} */
+ /** {@inheritDoc} */
public int describeContents() {
return 0;
}
- /** Implement the Parcelable interface {@hide} */
+ /** {@inheritDoc} */
public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(hwAddr);
- if (addr != null) {
+ dest.writeString(mHwAddr);
+ if (mAddr != null) {
dest.writeByte((byte)1);
- dest.writeParcelable(addr, flags);
+ dest.writeParcelable(mAddr, flags);
} else {
dest.writeByte((byte)0);
}
- dest.writeString(interfaceFlags);
+ dest.writeInt(mFlags.size());
+ for (String flag : mFlags) {
+ dest.writeString(flag);
+ }
}
- /** Implement the Parcelable interface {@hide} */
- public static final Creator<InterfaceConfiguration> CREATOR =
- new Creator<InterfaceConfiguration>() {
- public InterfaceConfiguration createFromParcel(Parcel in) {
- InterfaceConfiguration info = new InterfaceConfiguration();
- info.hwAddr = in.readString();
- if (in.readByte() == 1) {
- info.addr = in.readParcelable(null);
- }
- info.interfaceFlags = in.readString();
- return info;
+ public static final Creator<InterfaceConfiguration> CREATOR = new Creator<
+ InterfaceConfiguration>() {
+ public InterfaceConfiguration createFromParcel(Parcel in) {
+ InterfaceConfiguration info = new InterfaceConfiguration();
+ info.mHwAddr = in.readString();
+ if (in.readByte() == 1) {
+ info.mAddr = in.readParcelable(null);
}
-
- public InterfaceConfiguration[] newArray(int size) {
- return new InterfaceConfiguration[size];
+ final int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ info.mFlags.add(in.readString());
}
- };
+ return info;
+ }
+
+ public InterfaceConfiguration[] newArray(int size) {
+ return new InterfaceConfiguration[size];
+ }
+ };
+
+ private static void validateFlag(String flag) {
+ if (flag.indexOf(' ') >= 0) {
+ throw new IllegalArgumentException("flag contains space: " + flag);
+ }
+ }
}
diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java
index aa6400b..1a74abf 100644
--- a/core/java/android/net/NetworkIdentity.java
+++ b/core/java/android/net/NetworkIdentity.java
@@ -45,7 +45,7 @@ public class NetworkIdentity {
@Override
public int hashCode() {
- return Objects.hashCode(mType, mSubType, mSubscriberId);
+ return Objects.hashCode(mType, mSubType, mSubscriberId, mRoaming);
}
@Override
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
index 537750a..7286f0d 100644
--- a/core/java/android/net/NetworkInfo.java
+++ b/core/java/android/net/NetworkInfo.java
@@ -22,8 +22,9 @@ import android.os.Parcel;
import java.util.EnumMap;
/**
- * Describes the status of a network interface of a given type
- * (currently either Mobile or Wifi).
+ * Describes the status of a network interface.
+ * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents
+ * the current network connection.
*/
public class NetworkInfo implements Parcelable {
@@ -38,7 +39,7 @@ public class NetworkInfo implements Parcelable {
* <tr><td><code>SCANNING</code></td><td><code>CONNECTING</code></td></tr>
* <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr>
* <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr>
- * <tr><td><code>CONNECTED</code></td><td<code>CONNECTED</code></td></tr>
+ * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr>
* <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr>
* <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr>
* <tr><td><code>UNAVAILABLE</code></td><td><code>DISCONNECTED</code></td></tr>
@@ -159,9 +160,12 @@ public class NetworkInfo implements Parcelable {
}
/**
- * Reports the type of network (currently mobile or Wi-Fi) to which the
- * info in this object pertains.
- * @return the network type
+ * Reports the type of network to which the
+ * info in this {@code NetworkInfo} pertains.
+ * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link
+ * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link
+ * ConnectivityManager#TYPE_ETHERNET}, {@link ConnectivityManager#TYPE_BLUETOOTH}, or other
+ * types defined by {@link ConnectivityManager}
*/
public int getType() {
synchronized (this) {
@@ -226,6 +230,7 @@ public class NetworkInfo implements Parcelable {
/**
* Indicates whether network connectivity exists and it is possible to establish
* connections and pass data.
+ * <p>Always call this before attempting to perform data transactions.
* @return {@code true} if network connectivity exists, {@code false} otherwise.
*/
public boolean isConnected() {
diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java
index 1b24f0c..d9ea700 100644
--- a/core/java/android/net/NetworkPolicy.java
+++ b/core/java/android/net/NetworkPolicy.java
@@ -39,16 +39,18 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
public long warningBytes;
public long limitBytes;
public long lastSnooze;
+ public boolean metered;
private static final long DEFAULT_MTU = 1500;
public NetworkPolicy(NetworkTemplate template, int cycleDay, long warningBytes, long limitBytes,
- long lastSnooze) {
+ long lastSnooze, boolean metered) {
this.template = checkNotNull(template, "missing NetworkTemplate");
this.cycleDay = cycleDay;
this.warningBytes = warningBytes;
this.limitBytes = limitBytes;
this.lastSnooze = lastSnooze;
+ this.metered = metered;
}
public NetworkPolicy(Parcel in) {
@@ -57,6 +59,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
warningBytes = in.readLong();
limitBytes = in.readLong();
lastSnooze = in.readLong();
+ metered = in.readInt() != 0;
}
/** {@inheritDoc} */
@@ -66,6 +69,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
dest.writeLong(warningBytes);
dest.writeLong(limitBytes);
dest.writeLong(lastSnooze);
+ dest.writeInt(metered ? 1 : 0);
}
/** {@inheritDoc} */
@@ -99,16 +103,16 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
@Override
public int hashCode() {
- return Objects.hashCode(template, cycleDay, warningBytes, limitBytes, lastSnooze);
+ return Objects.hashCode(template, cycleDay, warningBytes, limitBytes, lastSnooze, metered);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof NetworkPolicy) {
final NetworkPolicy other = (NetworkPolicy) obj;
- return Objects.equal(template, other.template) && cycleDay == other.cycleDay
- && warningBytes == other.warningBytes && limitBytes == other.limitBytes
- && lastSnooze == other.lastSnooze;
+ return cycleDay == other.cycleDay && warningBytes == other.warningBytes
+ && limitBytes == other.limitBytes && lastSnooze == other.lastSnooze
+ && metered == other.metered && Objects.equal(template, other.template);
}
return false;
}
@@ -116,7 +120,8 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
@Override
public String toString() {
return "NetworkPolicy[" + template + "]: cycleDay=" + cycleDay + ", warningBytes="
- + warningBytes + ", limitBytes=" + limitBytes + ", lastSnooze=" + lastSnooze;
+ + warningBytes + ", limitBytes=" + limitBytes + ", lastSnooze=" + lastSnooze
+ + ", metered=" + metered;
}
public static final Creator<NetworkPolicy> CREATOR = new Creator<NetworkPolicy>() {
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index f6e627c..7a1ef66 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -16,8 +16,6 @@
package android.net;
-import static com.android.internal.util.Preconditions.checkNotNull;
-
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
@@ -40,8 +38,6 @@ import java.util.HashSet;
* @hide
*/
public class NetworkStats implements Parcelable {
- private static final String TAG = "NetworkStats";
-
/** {@link #iface} value when interface details unavailable. */
public static final String IFACE_ALL = null;
/** {@link #uid} value when UID details unavailable. */
@@ -106,6 +102,15 @@ public class NetworkStats implements Parcelable {
this.operations = operations;
}
+ public boolean isNegative() {
+ return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0;
+ }
+
+ public boolean isEmpty() {
+ return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0
+ && operations == 0;
+ }
+
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
@@ -347,6 +352,7 @@ public class NetworkStats implements Parcelable {
* on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface},
* since operation counts are at data layer.
*/
+ @Deprecated
public void spliceOperationsFrom(NetworkStats stats) {
for (int i = 0; i < size; i++) {
final int j = stats.findIndex(IFACE_ALL, uid[i], set[i], tag[i]);
@@ -401,7 +407,7 @@ public class NetworkStats implements Parcelable {
* Return total of all fields represented by this snapshot object.
*/
public Entry getTotal(Entry recycle) {
- return getTotal(recycle, null, UID_ALL);
+ return getTotal(recycle, null, UID_ALL, false);
}
/**
@@ -409,7 +415,7 @@ public class NetworkStats implements Parcelable {
* the requested {@link #uid}.
*/
public Entry getTotal(Entry recycle, int limitUid) {
- return getTotal(recycle, null, limitUid);
+ return getTotal(recycle, null, limitUid, false);
}
/**
@@ -417,7 +423,11 @@ public class NetworkStats implements Parcelable {
* the requested {@link #iface}.
*/
public Entry getTotal(Entry recycle, HashSet<String> limitIface) {
- return getTotal(recycle, limitIface, UID_ALL);
+ return getTotal(recycle, limitIface, UID_ALL, false);
+ }
+
+ public Entry getTotalIncludingTags(Entry recycle) {
+ return getTotal(recycle, null, UID_ALL, true);
}
/**
@@ -427,7 +437,8 @@ public class NetworkStats implements Parcelable {
* @param limitIface Set of {@link #iface} to include in total; or {@code
* null} to include all ifaces.
*/
- private Entry getTotal(Entry recycle, HashSet<String> limitIface, int limitUid) {
+ private Entry getTotal(
+ Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) {
final Entry entry = recycle != null ? recycle : new Entry();
entry.iface = IFACE_ALL;
@@ -446,7 +457,7 @@ public class NetworkStats implements Parcelable {
if (matchesUid && matchesIface) {
// skip specific tags, since already counted in TAG_NONE
- if (tag[i] != TAG_NONE) continue;
+ if (tag[i] != TAG_NONE && !includeTags) continue;
entry.rxBytes += rxBytes[i];
entry.rxPackets += rxPackets[i];
@@ -463,62 +474,64 @@ public class NetworkStats implements Parcelable {
* between two snapshots in time. Assumes that statistics rows collect over
* time, and that none of them have disappeared.
*/
- public NetworkStats subtract(NetworkStats value) throws NonMonotonicException {
- return subtract(value, false);
+ public NetworkStats subtract(NetworkStats right) {
+ return subtract(this, right, null, null);
}
/**
- * Subtract the given {@link NetworkStats}, effectively leaving the delta
+ * Subtract the two given {@link NetworkStats} objects, returning the delta
* between two snapshots in time. Assumes that statistics rows collect over
* time, and that none of them have disappeared.
- *
- * @param clampNonMonotonic When non-monotonic stats are found, just clamp
- * to 0 instead of throwing {@link NonMonotonicException}.
+ * <p>
+ * If counters have rolled backwards, they are clamped to {@code 0} and
+ * reported to the given {@link NonMonotonicObserver}.
*/
- public NetworkStats subtract(NetworkStats value, boolean clampNonMonotonic)
- throws NonMonotonicException {
- final long deltaRealtime = this.elapsedRealtime - value.elapsedRealtime;
+ public static <C> NetworkStats subtract(
+ NetworkStats left, NetworkStats right, NonMonotonicObserver<C> observer, C cookie) {
+ long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime;
if (deltaRealtime < 0) {
- throw new NonMonotonicException(this, value);
+ if (observer != null) {
+ observer.foundNonMonotonic(left, -1, right, -1, cookie);
+ }
+ deltaRealtime = 0;
}
// result will have our rows, and elapsed time between snapshots
final Entry entry = new Entry();
- final NetworkStats result = new NetworkStats(deltaRealtime, size);
- for (int i = 0; i < size; i++) {
- entry.iface = iface[i];
- entry.uid = uid[i];
- entry.set = set[i];
- entry.tag = tag[i];
+ final NetworkStats result = new NetworkStats(deltaRealtime, left.size);
+ for (int i = 0; i < left.size; i++) {
+ entry.iface = left.iface[i];
+ entry.uid = left.uid[i];
+ entry.set = left.set[i];
+ entry.tag = left.tag[i];
// find remote row that matches, and subtract
- final int j = value.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i);
+ final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i);
if (j == -1) {
// newly appearing row, return entire value
- entry.rxBytes = rxBytes[i];
- entry.rxPackets = rxPackets[i];
- entry.txBytes = txBytes[i];
- entry.txPackets = txPackets[i];
- entry.operations = operations[i];
+ entry.rxBytes = left.rxBytes[i];
+ entry.rxPackets = left.rxPackets[i];
+ entry.txBytes = left.txBytes[i];
+ entry.txPackets = left.txPackets[i];
+ entry.operations = left.operations[i];
} else {
// existing row, subtract remote value
- entry.rxBytes = rxBytes[i] - value.rxBytes[j];
- entry.rxPackets = rxPackets[i] - value.rxPackets[j];
- entry.txBytes = txBytes[i] - value.txBytes[j];
- entry.txPackets = txPackets[i] - value.txPackets[j];
- entry.operations = operations[i] - value.operations[j];
+ entry.rxBytes = left.rxBytes[i] - right.rxBytes[j];
+ entry.rxPackets = left.rxPackets[i] - right.rxPackets[j];
+ entry.txBytes = left.txBytes[i] - right.txBytes[j];
+ entry.txPackets = left.txPackets[i] - right.txPackets[j];
+ entry.operations = left.operations[i] - right.operations[j];
if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0
|| entry.txPackets < 0 || entry.operations < 0) {
- if (clampNonMonotonic) {
- entry.rxBytes = Math.max(entry.rxBytes, 0);
- entry.rxPackets = Math.max(entry.rxPackets, 0);
- entry.txBytes = Math.max(entry.txBytes, 0);
- entry.txPackets = Math.max(entry.txPackets, 0);
- entry.operations = Math.max(entry.operations, 0);
- } else {
- throw new NonMonotonicException(this, i, value, j);
+ if (observer != null) {
+ observer.foundNonMonotonic(left, i, right, j, cookie);
}
+ entry.rxBytes = Math.max(entry.rxBytes, 0);
+ entry.rxPackets = Math.max(entry.rxPackets, 0);
+ entry.txBytes = Math.max(entry.txBytes, 0);
+ entry.txPackets = Math.max(entry.txPackets, 0);
+ entry.operations = Math.max(entry.operations, 0);
}
}
@@ -665,22 +678,8 @@ public class NetworkStats implements Parcelable {
}
};
- public static class NonMonotonicException extends Exception {
- public final NetworkStats left;
- public final NetworkStats right;
- public final int leftIndex;
- public final int rightIndex;
-
- public NonMonotonicException(NetworkStats left, NetworkStats right) {
- this(left, -1, right, -1);
- }
-
- public NonMonotonicException(
- NetworkStats left, int leftIndex, NetworkStats right, int rightIndex) {
- this.left = checkNotNull(left, "missing left");
- this.right = checkNotNull(right, "missing right");
- this.leftIndex = leftIndex;
- this.rightIndex = rightIndex;
- }
+ public interface NonMonotonicObserver<C> {
+ public void foundNonMonotonic(
+ NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie);
}
}
diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java
index 8c01331..faf8a3f 100644
--- a/core/java/android/net/NetworkStatsHistory.java
+++ b/core/java/android/net/NetworkStatsHistory.java
@@ -26,16 +26,18 @@ import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray;
import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray;
import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray;
+import static com.android.internal.util.ArrayUtils.total;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.MathUtils;
+import com.android.internal.util.IndentingPrintWriter;
+
import java.io.CharArrayWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
-import java.io.PrintWriter;
import java.net.ProtocolException;
import java.util.Arrays;
import java.util.Random;
@@ -74,6 +76,7 @@ public class NetworkStatsHistory implements Parcelable {
private long[] txBytes;
private long[] txPackets;
private long[] operations;
+ private long totalBytes;
public static class Entry {
public static final long UNKNOWN = -1;
@@ -106,6 +109,12 @@ public class NetworkStatsHistory implements Parcelable {
if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize];
if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize];
bucketCount = 0;
+ totalBytes = 0;
+ }
+
+ public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) {
+ this(bucketDuration, existing.estimateResizeBuckets(bucketDuration));
+ recordEntireHistory(existing);
}
public NetworkStatsHistory(Parcel in) {
@@ -118,6 +127,7 @@ public class NetworkStatsHistory implements Parcelable {
txPackets = readLongArray(in);
operations = readLongArray(in);
bucketCount = bucketStart.length;
+ totalBytes = in.readLong();
}
/** {@inheritDoc} */
@@ -130,6 +140,7 @@ public class NetworkStatsHistory implements Parcelable {
writeLongArray(out, txBytes, bucketCount);
writeLongArray(out, txPackets, bucketCount);
writeLongArray(out, operations, bucketCount);
+ out.writeLong(totalBytes);
}
public NetworkStatsHistory(DataInputStream in) throws IOException {
@@ -144,6 +155,7 @@ public class NetworkStatsHistory implements Parcelable {
txPackets = new long[bucketStart.length];
operations = new long[bucketStart.length];
bucketCount = bucketStart.length;
+ totalBytes = total(rxBytes) + total(txBytes);
break;
}
case VERSION_ADD_PACKETS:
@@ -158,6 +170,7 @@ public class NetworkStatsHistory implements Parcelable {
txPackets = readVarLongArray(in);
operations = readVarLongArray(in);
bucketCount = bucketStart.length;
+ totalBytes = total(rxBytes) + total(txBytes);
break;
}
default: {
@@ -208,6 +221,13 @@ public class NetworkStatsHistory implements Parcelable {
}
/**
+ * Return total bytes represented by this history.
+ */
+ public long getTotalBytes() {
+ return totalBytes;
+ }
+
+ /**
* Return index of bucket that contains or is immediately before the
* requested time.
*/
@@ -266,13 +286,16 @@ public class NetworkStatsHistory implements Parcelable {
* distribute across internal buckets, creating new buckets as needed.
*/
public void recordData(long start, long end, NetworkStats.Entry entry) {
- if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0
- || entry.operations < 0) {
+ long rxBytes = entry.rxBytes;
+ long rxPackets = entry.rxPackets;
+ long txBytes = entry.txBytes;
+ long txPackets = entry.txPackets;
+ long operations = entry.operations;
+
+ if (entry.isNegative()) {
throw new IllegalArgumentException("tried recording negative data");
}
- if (entry.rxBytes == 0 && entry.rxPackets == 0 && entry.txBytes == 0 && entry.txPackets == 0
- && entry.operations == 0) {
- // nothing to record; skip
+ if (entry.isEmpty()) {
return;
}
@@ -295,21 +318,23 @@ public class NetworkStatsHistory implements Parcelable {
if (overlap <= 0) continue;
// integer math each time is faster than floating point
- final long fracRxBytes = entry.rxBytes * overlap / duration;
- final long fracRxPackets = entry.rxPackets * overlap / duration;
- final long fracTxBytes = entry.txBytes * overlap / duration;
- final long fracTxPackets = entry.txPackets * overlap / duration;
- final long fracOperations = entry.operations * overlap / duration;
+ final long fracRxBytes = rxBytes * overlap / duration;
+ final long fracRxPackets = rxPackets * overlap / duration;
+ final long fracTxBytes = txBytes * overlap / duration;
+ final long fracTxPackets = txPackets * overlap / duration;
+ final long fracOperations = operations * overlap / duration;
addLong(activeTime, i, overlap);
- addLong(rxBytes, i, fracRxBytes); entry.rxBytes -= fracRxBytes;
- addLong(rxPackets, i, fracRxPackets); entry.rxPackets -= fracRxPackets;
- addLong(txBytes, i, fracTxBytes); entry.txBytes -= fracTxBytes;
- addLong(txPackets, i, fracTxPackets); entry.txPackets -= fracTxPackets;
- addLong(operations, i, fracOperations); entry.operations -= fracOperations;
+ addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes;
+ addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets;
+ addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes;
+ addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets;
+ addLong(this.operations, i, fracOperations); operations -= fracOperations;
duration -= overlap;
}
+
+ totalBytes += entry.rxBytes + entry.txBytes;
}
/**
@@ -394,6 +419,7 @@ public class NetworkStatsHistory implements Parcelable {
/**
* Remove buckets older than requested cutoff.
*/
+ @Deprecated
public void removeBucketsBefore(long cutoff) {
int i;
for (i = 0; i < bucketCount; i++) {
@@ -415,6 +441,8 @@ public class NetworkStatsHistory implements Parcelable {
if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length);
if (operations != null) operations = Arrays.copyOfRange(operations, i, length);
bucketCount -= i;
+
+ // TODO: subtract removed values from totalBytes
}
}
@@ -527,19 +555,17 @@ public class NetworkStatsHistory implements Parcelable {
return (long) (start + (r.nextFloat() * (end - start)));
}
- public void dump(String prefix, PrintWriter pw, boolean fullHistory) {
- pw.print(prefix);
+ public void dump(IndentingPrintWriter pw, boolean fullHistory) {
pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration);
+ pw.increaseIndent();
final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
if (start > 0) {
- pw.print(prefix);
- pw.print(" (omitting "); pw.print(start); pw.println(" buckets)");
+ pw.print("(omitting "); pw.print(start); pw.println(" buckets)");
}
for (int i = start; i < bucketCount; i++) {
- pw.print(prefix);
- pw.print(" bucketStart="); pw.print(bucketStart[i]);
+ pw.print("bucketStart="); pw.print(bucketStart[i]);
if (activeTime != null) { pw.print(" activeTime="); pw.print(activeTime[i]); }
if (rxBytes != null) { pw.print(" rxBytes="); pw.print(rxBytes[i]); }
if (rxPackets != null) { pw.print(" rxPackets="); pw.print(rxPackets[i]); }
@@ -548,12 +574,14 @@ public class NetworkStatsHistory implements Parcelable {
if (operations != null) { pw.print(" operations="); pw.print(operations[i]); }
pw.println();
}
+
+ pw.decreaseIndent();
}
@Override
public String toString() {
final CharArrayWriter writer = new CharArrayWriter();
- dump("", new PrintWriter(writer), false);
+ dump(new IndentingPrintWriter(writer, " "), false);
return writer.toString();
}
@@ -579,6 +607,10 @@ public class NetworkStatsHistory implements Parcelable {
if (array != null) array[i] += value;
}
+ public int estimateResizeBuckets(long newBucketDuration) {
+ return (int) (size() * getBucketDuration() / newBucketDuration);
+ }
+
/**
* Utility methods for interacting with {@link DataInputStream} and
* {@link DataOutputStream}, mostly dealing with writing partial arrays.
diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java
index 418b82f..8ebfd8d 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/core/java/android/net/NetworkTemplate.java
@@ -18,6 +18,7 @@ package android.net;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
import static android.net.ConnectivityManager.TYPE_WIMAX;
import static android.net.NetworkIdentity.scrubSubscriberId;
import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G;
@@ -231,10 +232,13 @@ public class NetworkTemplate implements Parcelable {
* Check if matches Wi-Fi network template.
*/
private boolean matchesWifi(NetworkIdentity ident) {
- if (ident.mType == TYPE_WIFI) {
- return true;
+ switch (ident.mType) {
+ case TYPE_WIFI:
+ case TYPE_WIFI_P2P:
+ return true;
+ default:
+ return false;
}
- return false;
}
/**
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index cd585b2..dfdea38 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -20,7 +20,6 @@ import android.app.DownloadManager;
import android.app.backup.BackupManager;
import android.content.Context;
import android.media.MediaPlayer;
-import android.net.NetworkStats.NonMonotonicException;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -193,15 +192,12 @@ public class TrafficStats {
throw new IllegalStateException("not profiling data");
}
- try {
- // subtract starting values and return delta
- final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
- final NetworkStats profilingDelta = profilingStop.subtract(sActiveProfilingStart);
- sActiveProfilingStart = null;
- return profilingDelta;
- } catch (NonMonotonicException e) {
- throw new RuntimeException(e);
- }
+ // subtract starting values and return delta
+ final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
+ final NetworkStats profilingDelta = NetworkStats.subtract(
+ profilingStop, sActiveProfilingStart, null, null);
+ sActiveProfilingStart = null;
+ return profilingDelta;
}
}
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
index 9d28eff..defe7aa 100644
--- a/core/java/android/net/Uri.java
+++ b/core/java/android/net/Uri.java
@@ -19,19 +19,19 @@ package android.net;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
-
import java.io.File;
-import java.io.IOException;
import java.io.UnsupportedEncodingException;
-import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
+import java.nio.charset.Charsets;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Locale;
import java.util.RandomAccess;
import java.util.Set;
+import libcore.net.UriCodec;
/**
* Immutable URI reference. A URI reference includes a URI and a fragment, the
@@ -1305,7 +1305,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
*
* <p>An opaque URI follows this pattern:
* {@code <scheme>:<opaque part>#<fragment>}
- *
+ *
* <p>Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI.
*/
public static final class Builder {
@@ -1646,6 +1646,9 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
/**
* Searches the query string for the first value with the given key.
*
+ * <p><strong>Warning:</strong> Prior to Ice Cream Sandwich, this decoded
+ * the '+' character as '+' rather than ' '.
+ *
* @param key which will be encoded
* @throws UnsupportedOperationException if this isn't a hierarchical URI
* @throws NullPointerException if key is null
@@ -1679,9 +1682,10 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
if (separator - start == encodedKey.length()
&& query.regionMatches(start, encodedKey, 0, encodedKey.length())) {
if (separator == end) {
- return "";
+ return "";
} else {
- return decode(query.substring(separator + 1, end));
+ String encodedValue = query.substring(separator + 1, end);
+ return UriCodec.decode(encodedValue, true, Charsets.UTF_8, false);
}
}
@@ -1713,6 +1717,38 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
return (!"false".equals(flag) && !"0".equals(flag));
}
+ /**
+ * Return a normalized representation of this Uri.
+ *
+ * <p>A normalized Uri has a lowercase scheme component.
+ * This aligns the Uri with Android best practices for
+ * intent filtering.
+ *
+ * <p>For example, "HTTP://www.android.com" becomes
+ * "http://www.android.com"
+ *
+ * <p>All URIs received from outside Android (such as user input,
+ * or external sources like Bluetooth, NFC, or the Internet) should
+ * be normalized before they are used to create an Intent.
+ *
+ * <p class="note">This method does <em>not</em> validate bad URI's,
+ * or 'fix' poorly formatted URI's - so do not use it for input validation.
+ * A Uri will always be returned, even if the Uri is badly formatted to
+ * begin with and a scheme component cannot be found.
+ *
+ * @return normalized Uri (never null)
+ * @see {@link android.content.Intent#setData}
+ * @see {@link #setNormalizedData}
+ */
+ public Uri normalize() {
+ String scheme = getScheme();
+ if (scheme == null) return this; // give up
+ String lowerScheme = scheme.toLowerCase(Locale.US);
+ if (scheme.equals(lowerScheme)) return this; // no change
+
+ return buildUpon().scheme(lowerScheme).build();
+ }
+
/** Identifies a null parcelled Uri. */
private static final int NULL_TYPE_ID = 0;
@@ -1877,9 +1913,6 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
|| (allow != null && allow.indexOf(c) != NOT_FOUND);
}
- /** Unicode replacement character: \\uFFFD. */
- private static final byte[] REPLACEMENT = { (byte) 0xFF, (byte) 0xFD };
-
/**
* Decodes '%'-escaped octets in the given string using the UTF-8 scheme.
* Replaces invalid octets with the unicode replacement character
@@ -1890,104 +1923,10 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
* s is null
*/
public static String decode(String s) {
- /*
- Compared to java.net.URLEncoderDecoder.decode(), this method decodes a
- chunk at a time instead of one character at a time, and it doesn't
- throw exceptions. It also only allocates memory when necessary--if
- there's nothing to decode, this method won't do much.
- */
-
if (s == null) {
return null;
}
-
- // Lazily-initialized buffers.
- StringBuilder decoded = null;
- ByteArrayOutputStream out = null;
-
- int oldLength = s.length();
-
- // This loop alternates between copying over normal characters and
- // escaping in chunks. This results in fewer method calls and
- // allocations than decoding one character at a time.
- int current = 0;
- while (current < oldLength) {
- // Start in "copying" mode where we copy over normal characters.
-
- // Find the next escape sequence.
- int nextEscape = s.indexOf('%', current);
-
- if (nextEscape == NOT_FOUND) {
- if (decoded == null) {
- // We didn't actually decode anything.
- return s;
- } else {
- // Append the remainder and return the decoded string.
- decoded.append(s, current, oldLength);
- return decoded.toString();
- }
- }
-
- // Prepare buffers.
- if (decoded == null) {
- // Looks like we're going to need the buffers...
- // We know the new string will be shorter. Using the old length
- // may overshoot a bit, but it will save us from resizing the
- // buffer.
- decoded = new StringBuilder(oldLength);
- out = new ByteArrayOutputStream(4);
- } else {
- // Clear decoding buffer.
- out.reset();
- }
-
- // Append characters leading up to the escape.
- if (nextEscape > current) {
- decoded.append(s, current, nextEscape);
-
- current = nextEscape;
- } else {
- // assert current == nextEscape
- }
-
- // Switch to "decoding" mode where we decode a string of escape
- // sequences.
-
- // Decode and append escape sequences. Escape sequences look like
- // "%ab" where % is literal and a and b are hex digits.
- try {
- do {
- if (current + 2 >= oldLength) {
- // Truncated escape sequence.
- out.write(REPLACEMENT);
- } else {
- int a = Character.digit(s.charAt(current + 1), 16);
- int b = Character.digit(s.charAt(current + 2), 16);
-
- if (a == -1 || b == -1) {
- // Non hex digits.
- out.write(REPLACEMENT);
- } else {
- // Combine the hex digits into one byte and write.
- out.write((a << 4) + b);
- }
- }
-
- // Move passed the escape sequence.
- current += 3;
- } while (current < oldLength && s.charAt(current) == '%');
-
- // Decode UTF-8 bytes into a string and append it.
- decoded.append(out.toString(DEFAULT_ENCODING));
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError(e);
- } catch (IOException e) {
- throw new AssertionError(e);
- }
- }
-
- // If we don't have a buffer, we didn't have to decode anything.
- return decoded == null ? s : decoded.toString();
+ return UriCodec.decode(s, false, Charsets.UTF_8, false);
}
/**
@@ -2342,7 +2281,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> {
*
* @param baseUri Uri to append path segment to
* @param pathSegment encoded path segment to append
- * @return a new Uri based on baseUri with the given segment appended to
+ * @return a new Uri based on baseUri with the given segment appended to
* the path
* @throws NullPointerException if baseUri is null
*/
diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java
index 92be373..f94d320 100644
--- a/core/java/android/net/http/CertificateChainValidator.java
+++ b/core/java/android/net/http/CertificateChainValidator.java
@@ -17,32 +17,20 @@
package android.net.http;
-import com.android.internal.net.DomainNameValidator;
-
-import org.apache.harmony.security.provider.cert.X509CertImpl;
-import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
-
import java.io.IOException;
-
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateExpiredException;
-import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.util.Date;
-
+import javax.net.ssl.DefaultHostnameVerifier;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
+import org.apache.harmony.security.provider.cert.X509CertImpl;
+import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
/**
* Class responsible for all server certificate validation functionality
- *
+ *
* {@hide}
*/
class CertificateChainValidator {
@@ -53,6 +41,9 @@ class CertificateChainValidator {
private static final CertificateChainValidator sInstance
= new CertificateChainValidator();
+ private static final DefaultHostnameVerifier sVerifier
+ = new DefaultHostnameVerifier();
+
/**
* @return The singleton instance of the certificates chain validator
*/
@@ -147,7 +138,10 @@ class CertificateChainValidator {
throw new IllegalArgumentException("certificate for this site is null");
}
- if (!DomainNameValidator.match(currCertificate, domain)) {
+ boolean valid = domain != null
+ && !domain.isEmpty()
+ && sVerifier.verify(domain, currCertificate);
+ if (!valid) {
if (HttpLog.LOGV) {
HttpLog.v("certificate not for this host: " + domain);
}
diff --git a/core/java/android/net/http/HttpResponseCache.java b/core/java/android/net/http/HttpResponseCache.java
index 5f65dfa..21736aa 100644
--- a/core/java/android/net/http/HttpResponseCache.java
+++ b/core/java/android/net/http/HttpResponseCache.java
@@ -136,6 +136,18 @@ import org.apache.http.impl.client.DefaultHttpClient;
* int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
* connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
* }</pre>
+ *
+ * <h3>Working With Earlier Releases</h3>
+ * This class was added in Android 4.0 (Ice Cream Sandwich). Use reflection to
+ * enable the response cache without impacting earlier releases: <pre> {@code
+ * try {
+ * File httpCacheDir = new File(context.getCacheDir(), "http");
+ * long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
+ * Class.forName("android.net.http.HttpResponseCache")
+ * .getMethod("install", File.class, long.class)
+ * .invoke(null, httpCacheDir, httpCacheSize);
+ * } catch (Exception httpResponseCacheNotAvailable) {
+ * }}</pre>
*/
public final class HttpResponseCache extends ResponseCache implements Closeable {
diff --git a/core/java/android/nfc/FormatException.java b/core/java/android/nfc/FormatException.java
index 7045a03..a57de1e 100644
--- a/core/java/android/nfc/FormatException.java
+++ b/core/java/android/nfc/FormatException.java
@@ -24,4 +24,8 @@ public class FormatException extends Exception {
public FormatException(String message) {
super(message);
}
+
+ public FormatException(String message, Throwable e) {
+ super(message, e);
+ }
}
diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl
index 0b93ad0..61bc324 100644
--- a/core/java/android/nfc/INfcAdapter.aidl
+++ b/core/java/android/nfc/INfcAdapter.aidl
@@ -17,7 +17,6 @@
package android.nfc;
import android.app.PendingIntent;
-import android.content.ComponentName;
import android.content.IntentFilter;
import android.nfc.NdefMessage;
import android.nfc.Tag;
@@ -44,4 +43,6 @@ interface INfcAdapter
void setForegroundDispatch(in PendingIntent intent,
in IntentFilter[] filters, in TechListParcel techLists);
void setForegroundNdefPush(in NdefMessage msg, in INdefPushCallback callback);
+
+ void dispatch(in Tag tag);
}
diff --git a/core/java/android/nfc/LlcpPacket.java b/core/java/android/nfc/LlcpPacket.java
deleted file mode 100644
index 9919dc4..0000000
--- a/core/java/android/nfc/LlcpPacket.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.nfc;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * Represents a LLCP packet received in a LLCP Connectionless communication;
- * @hide
- */
-public class LlcpPacket implements Parcelable {
-
- private final int mRemoteSap;
-
- private final byte[] mDataBuffer;
-
- /**
- * Creates a LlcpPacket to be sent to a remote Service Access Point number
- * (SAP)
- *
- * @param sap Remote Service Access Point number
- * @param data Data buffer
- */
- public LlcpPacket(int sap, byte[] data) {
- mRemoteSap = sap;
- mDataBuffer = data;
- }
-
- /**
- * Returns the remote Service Access Point number
- */
- public int getRemoteSap() {
- return mRemoteSap;
- }
-
- /**
- * Returns the data buffer
- */
- public byte[] getDataBuffer() {
- return mDataBuffer;
- }
-
- public int describeContents() {
- return 0;
- }
-
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mRemoteSap);
- dest.writeInt(mDataBuffer.length);
- dest.writeByteArray(mDataBuffer);
- }
-
- public static final Parcelable.Creator<LlcpPacket> CREATOR = new Parcelable.Creator<LlcpPacket>() {
- public LlcpPacket createFromParcel(Parcel in) {
- // Remote SAP
- short sap = (short)in.readInt();
-
- // Data Buffer
- int dataLength = in.readInt();
- byte[] data = new byte[dataLength];
- in.readByteArray(data);
-
- return new LlcpPacket(sap, data);
- }
-
- public LlcpPacket[] newArray(int size) {
- return new LlcpPacket[size];
- }
- };
-} \ No newline at end of file
diff --git a/core/java/android/nfc/NdefMessage.java b/core/java/android/nfc/NdefMessage.java
index c79fabf..5df9272 100644
--- a/core/java/android/nfc/NdefMessage.java
+++ b/core/java/android/nfc/NdefMessage.java
@@ -16,90 +16,190 @@
package android.nfc;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
import android.os.Parcel;
import android.os.Parcelable;
+
/**
- * Represents an NDEF (NFC Data Exchange Format) data message that contains one or more {@link
- * NdefRecord}s.
- * <p>An NDEF message includes "records" that can contain different sets of data, such as
- * MIME-type media, a URI, or one of the supported RTD types (see {@link NdefRecord}). An NDEF
- * message always contains zero or more NDEF records.</p>
- * <p>This is an immutable data class.
+ * Represents an immutable NDEF Message.
+ * <p>
+ * NDEF (NFC Data Exchange Format) is a light-weight binary format,
+ * used to encapsulate typed data. It is specified by the NFC Forum,
+ * for transmission and storage with NFC, however it is transport agnostic.
+ * <p>
+ * NDEF defines messages and records. An NDEF Record contains
+ * typed data, such as MIME-type media, a URI, or a custom
+ * application payload. An NDEF Message is a container for
+ * one or more NDEF Records.
+ * <p>
+ * When an Android device receives an NDEF Message
+ * (for example by reading an NFC tag) it processes it through
+ * a dispatch mechanism to determine an activity to launch.
+ * The type of the <em>first</em> record in the message has
+ * special importance for message dispatch, so design this record
+ * carefully.
+ * <p>
+ * Use {@link #NdefMessage(byte[])} to construct an NDEF Message from
+ * binary data, or {@link #NdefMessage(NdefRecord[])} to
+ * construct from one or more {@link NdefRecord}s.
+ * <p class="note">
+ * {@link NdefMessage} and {@link NdefRecord} implementations are
+ * always available, even on Android devices that do not have NFC hardware.
+ * <p class="note">
+ * {@link NdefRecord}s are intended to be immutable (and thread-safe),
+ * however they may contain mutable fields. So take care not to modify
+ * mutable fields passed into constructors, or modify mutable fields
+ * obtained by getter methods, unless such modification is explicitly
+ * marked as safe.
+ *
+ * @see NfcAdapter#ACTION_NDEF_DISCOVERED
+ * @see NdefRecord
*/
public final class NdefMessage implements Parcelable {
- private static final byte FLAG_MB = (byte) 0x80;
- private static final byte FLAG_ME = (byte) 0x40;
-
private final NdefRecord[] mRecords;
/**
- * Create an NDEF message from raw bytes.
- * <p>
- * Validation is performed to make sure the Record format headers are valid,
- * and the ID + TYPE + PAYLOAD fields are of the correct size.
- * @throws FormatException
+ * Construct an NDEF Message by parsing raw bytes.<p>
+ * Strict validation of the NDEF binary structure is performed:
+ * there must be at least one record, every record flag must
+ * be correct, and the total length of the message must match
+ * the length of the input data.<p>
+ * This parser can handle chunked records, and converts them
+ * into logical {@link NdefRecord}s within the message.<p>
+ * Once the input data has been parsed to one or more logical
+ * records, basic validation of the tnf, type, id, and payload fields
+ * of each record is performed, as per the documentation on
+ * on {@link NdefRecord#NdefRecord(short, byte[], byte[], byte[])}<p>
+ * If either strict validation of the binary format fails, or
+ * basic validation during record construction fails, a
+ * {@link FormatException} is thrown<p>
+ * Deep inspection of the type, id and payload fields of
+ * each record is not performed, so it is possible to parse input
+ * that has a valid binary format and confirms to the basic
+ * validation requirements of
+ * {@link NdefRecord#NdefRecord(short, byte[], byte[], byte[])},
+ * but fails more strict requirements as specified by the
+ * NFC Forum.
+ *
+ * <p class="note">
+ * It is safe to re-use the data byte array after construction:
+ * this constructor will make an internal copy of all necessary fields.
+ *
+ * @param data raw bytes to parse
+ * @throws FormatException if the data cannot be parsed
*/
public NdefMessage(byte[] data) throws FormatException {
- mRecords = null; // stop compiler complaints about final field
- if (parseNdefMessage(data) == -1) {
- throw new FormatException("Error while parsing NDEF message");
+ if (data == null) throw new NullPointerException("data is null");
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+
+ mRecords = NdefRecord.parse(buffer, false);
+
+ if (buffer.remaining() > 0) {
+ throw new FormatException("trailing data");
+ }
+ }
+
+ /**
+ * Construct an NDEF Message from one or more NDEF Records.
+ *
+ * @param record first record (mandatory)
+ * @param records additional records (optional)
+ */
+ public NdefMessage(NdefRecord record, NdefRecord ... records) {
+ // validate
+ if (record == null) throw new NullPointerException("record cannot be null");
+
+ for (NdefRecord r : records) {
+ if (r == null) {
+ throw new NullPointerException("record cannot be null");
+ }
}
+
+ mRecords = new NdefRecord[1 + records.length];
+ mRecords[0] = record;
+ System.arraycopy(records, 0, mRecords, 1, records.length);
}
/**
- * Create an NDEF message from NDEF records.
+ * Construct an NDEF Message from one or more NDEF Records.
+ *
+ * @param records one or more records
*/
public NdefMessage(NdefRecord[] records) {
- mRecords = new NdefRecord[records.length];
- System.arraycopy(records, 0, mRecords, 0, records.length);
+ // validate
+ if (records.length < 1) {
+ throw new IllegalArgumentException("must have at least one record");
+ }
+ for (NdefRecord r : records) {
+ if (r == null) {
+ throw new NullPointerException("records cannot contain null");
+ }
+ }
+
+ mRecords = records;
}
/**
- * Get the NDEF records inside this NDEF message.
+ * Get the NDEF Records inside this NDEF Message.<p>
+ * An {@link NdefMessage} always has one or more NDEF Records: so the
+ * following code to retrieve the first record is always safe
+ * (no need to check for null or array length >= 1):
+ * <pre>
+ * NdefRecord firstRecord = ndefMessage.getRecords()[0];
+ * </pre>
*
- * @return array of zero or more NDEF records.
+ * @return array of one or more NDEF records.
*/
public NdefRecord[] getRecords() {
- return mRecords.clone();
+ return mRecords;
}
/**
- * Returns a byte array representation of this entire NDEF message.
+ * Return the length of this NDEF Message if it is written to a byte array
+ * with {@link #toByteArray}.<p>
+ * An NDEF Message can be formatted to bytes in different ways
+ * depending on chunking, SR, and ID flags, so the length returned
+ * by this method may not be equal to the length of the original
+ * byte array used to construct this NDEF Message. However it will
+ * always be equal to the length of the byte array produced by
+ * {@link #toByteArray}.
+ *
+ * @return length of this NDEF Message when written to bytes with {@link #toByteArray}
+ * @see #toByteArray
*/
- public byte[] toByteArray() {
- //TODO: allocate the byte array once, copy each record once
- //TODO: process MB and ME flags outside loop
- if ((mRecords == null) || (mRecords.length == 0))
- return new byte[0];
-
- byte[] msg = {};
-
- for (int i = 0; i < mRecords.length; i++) {
- byte[] record = mRecords[i].toByteArray();
- byte[] tmp = new byte[msg.length + record.length];
-
- /* Make sure the Message Begin flag is set only for the first record */
- if (i == 0) {
- record[0] |= FLAG_MB;
- } else {
- record[0] &= ~FLAG_MB;
- }
-
- /* Make sure the Message End flag is set only for the last record */
- if (i == (mRecords.length - 1)) {
- record[0] |= FLAG_ME;
- } else {
- record[0] &= ~FLAG_ME;
- }
+ public int getByteArrayLength() {
+ int length = 0;
+ for (NdefRecord r : mRecords) {
+ length += r.getByteLength();
+ }
+ return length;
+ }
- System.arraycopy(msg, 0, tmp, 0, msg.length);
- System.arraycopy(record, 0, tmp, msg.length, record.length);
+ /**
+ * Return this NDEF Message as raw bytes.<p>
+ * The NDEF Message is formatted as per the NDEF 1.0 specification,
+ * and the byte array is suitable for network transmission or storage
+ * in an NFC Forum NDEF compatible tag.<p>
+ * This method will not chunk any records, and will always use the
+ * short record (SR) format and omit the identifier field when possible.
+ *
+ * @return NDEF Message in binary format
+ * @see getByteArrayLength
+ */
+ public byte[] toByteArray() {
+ int length = getByteArrayLength();
+ ByteBuffer buffer = ByteBuffer.allocate(length);
- msg = tmp;
+ for (int i=0; i<mRecords.length; i++) {
+ boolean mb = (i == 0); // first record
+ boolean me = (i == mRecords.length - 1); // last record
+ mRecords[i].writeToByteBuffer(buffer, mb, me);
}
- return msg;
+ return buffer.array();
}
@Override
@@ -128,5 +228,26 @@ public final class NdefMessage implements Parcelable {
}
};
- private native int parseNdefMessage(byte[] data);
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mRecords);
+ }
+
+ /**
+ * Returns true if the specified NDEF Message contains
+ * identical NDEF Records.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ NdefMessage other = (NdefMessage) obj;
+ return Arrays.equals(mRecords, other.mRecords);
+ }
+
+ @Override
+ public String toString() {
+ return "NdefMessage " + Arrays.toString(mRecords);
+ }
} \ No newline at end of file
diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java
index 26571ff..0e9e8f4 100644
--- a/core/java/android/nfc/NdefRecord.java
+++ b/core/java/android/nfc/NdefRecord.java
@@ -16,83 +16,144 @@
package android.nfc;
+import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
-
-import java.lang.UnsupportedOperationException;
-import java.nio.charset.Charset;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
import java.nio.charset.Charsets;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
/**
- * Represents a logical (unchunked) NDEF (NFC Data Exchange Format) record.
- * <p>An NDEF record always contains:
+ * Represents an immutable NDEF Record.
+ * <p>
+ * NDEF (NFC Data Exchange Format) is a light-weight binary format,
+ * used to encapsulate typed data. It is specified by the NFC Forum,
+ * for transmission and storage with NFC, however it is transport agnostic.
+ * <p>
+ * NDEF defines messages and records. An NDEF Record contains
+ * typed data, such as MIME-type media, a URI, or a custom
+ * application payload. An NDEF Message is a container for
+ * one or more NDEF Records.
+ * <p>
+ * This class represents logical (complete) NDEF Records, and can not be
+ * used to represent chunked (partial) NDEF Records. However
+ * {@link NdefMessage#NdefMessage(byte[])} can be used to parse a message
+ * containing chunked records, and will return a message with unchunked
+ * (complete) records.
+ * <p>
+ * A logical NDEF Record always contains a 3-bit TNF (Type Name Field)
+ * that provides high level typing for the rest of the record. The
+ * remaining fields are variable length and not always present:
* <ul>
- * <li>3-bit TNF (Type Name Format) field: Indicates how to interpret the type field
- * <li>Variable length type: Describes the record format
- * <li>Variable length ID: A unique identifier for the record
- * <li>Variable length payload: The actual data payload
+ * <li><em>type</em>: detailed typing for the payload</li>
+ * <li><em>id</em>: identifier meta-data, not commonly used</li>
+ * <li><em>payload</em>: the actual payload</li>
* </ul>
- * <p>The underlying record
- * representation may be chunked across several NDEF records when the payload is
- * large.
- * <p>This is an immutable data class.
+ * <p>
+ * Helpers such as {@link NdefRecord#createUri}, {@link NdefRecord#createMime}
+ * and {@link NdefRecord#createExternal} are included to create well-formatted
+ * NDEF Records with correctly set tnf, type, id and payload fields, please
+ * use these helpers whenever possible.
+ * <p>
+ * Use the constructor {@link #NdefRecord(short, byte[], byte[], byte[])}
+ * if you know what you are doing and what to set the fields individually.
+ * Only basic validation is performed with this constructor, so it is possible
+ * to create records that do not confirm to the strict NFC Forum
+ * specifications.
+ * <p>
+ * The binary representation of an NDEF Record includes additional flags to
+ * indicate location with an NDEF message, provide support for chunking of
+ * NDEF records, and to pack optional fields. This class does not expose
+ * those details. To write an NDEF Record as binary you must first put it
+ * into an @{link NdefMessage}, then call {@link NdefMessage#toByteArray()}.
+ * <p class="note">
+ * {@link NdefMessage} and {@link NdefRecord} implementations are
+ * always available, even on Android devices that do not have NFC hardware.
+ * <p class="note">
+ * {@link NdefRecord}s are intended to be immutable (and thread-safe),
+ * however they may contain mutable fields. So take care not to modify
+ * mutable fields passed into constructors, or modify mutable fields
+ * obtained by getter methods, unless such modification is explicitly
+ * marked as safe.
+ *
+ * @see NfcAdapter#ACTION_NDEF_DISCOVERED
+ * @see NdefMessage
*/
public final class NdefRecord implements Parcelable {
/**
- * Indicates no type, id, or payload is associated with this NDEF Record.
- * <p>
- * Type, id and payload fields must all be empty to be a valid TNF_EMPTY
- * record.
+ * Indicates the record is empty.<p>
+ * Type, id and payload fields are empty in a {@literal TNF_EMPTY} record.
*/
public static final short TNF_EMPTY = 0x00;
/**
- * Indicates the type field uses the RTD type name format.
+ * Indicates the type field contains a well-known RTD type name.<p>
+ * Use this tnf with RTD types such as {@link #RTD_TEXT}, {@link #RTD_URI}.
* <p>
- * Use this TNF with RTD types such as RTD_TEXT, RTD_URI.
+ * The RTD type name format is specified in NFCForum-TS-RTD_1.0.
+ *
+ * @see #RTD_URI
+ * @see #RTD_TEXT
+ * @see #RTD_SMART_POSTER
+ * @see #createUri
*/
public static final short TNF_WELL_KNOWN = 0x01;
/**
- * Indicates the type field contains a value that follows the media-type BNF
- * construct defined by RFC 2046.
+ * Indicates the type field contains a media-type BNF
+ * construct, defined by RFC 2046.<p>
+ * Use this with MIME type names such as {@literal "image/jpeg"}, or
+ * using the helper {@link #createMime}.
+ *
+ * @see #createMime
*/
public static final short TNF_MIME_MEDIA = 0x02;
/**
- * Indicates the type field contains a value that follows the absolute-URI
- * BNF construct defined by RFC 3986.
+ * Indicates the type field contains an absolute-URI
+ * BNF construct defined by RFC 3986.<p>
+ * When creating new records prefer {@link #createUri},
+ * since it offers more compact URI encoding
+ * ({@literal #RTD_URI} allows compression of common URI prefixes).
+ *
+ * @see #createUri
*/
public static final short TNF_ABSOLUTE_URI = 0x03;
/**
- * Indicates the type field contains a value that follows the RTD external
- * name specification.
+ * Indicates the type field contains an external type name.<p>
+ * Used to encode custom payloads. When creating new records
+ * use the helper {@link #createExternal}.<p>
+ * The external-type RTD format is specified in NFCForum-TS-RTD_1.0.<p>
* <p>
* Note this TNF should not be used with RTD_TEXT or RTD_URI constants.
* Those are well known RTD constants, not external RTD constants.
+ *
+ * @see #createExternal
*/
public static final short TNF_EXTERNAL_TYPE = 0x04;
/**
- * Indicates the payload type is unknown.
- * <p>
- * This is similar to the "application/octet-stream" MIME type. The payload
- * type is not explicitly encoded within the NDEF Message.
+ * Indicates the payload type is unknown.<p>
+ * NFC Forum explains this should be treated similarly to the
+ * "application/octet-stream" MIME type. The payload
+ * type is not explicitly encoded within the record.
* <p>
- * The type field must be empty to be a valid TNF_UNKNOWN record.
+ * The type field is empty in an {@literal TNF_UNKNOWN} record.
*/
public static final short TNF_UNKNOWN = 0x05;
/**
* Indicates the payload is an intermediate or final chunk of a chunked
- * NDEF Record.
- * <p>
- * The payload type is specified in the first chunk, and subsequent chunks
- * must use TNF_UNCHANGED with an empty type field. TNF_UNCHANGED must not
- * be used in any other situation.
+ * NDEF Record.<p>
+ * {@literal TNF_UNCHANGED} can not be used with this class
+ * since all {@link NdefRecord}s are already unchunked, however they
+ * may appear in the binary format.
*/
public static final short TNF_UNCHANGED = 0x06;
@@ -106,42 +167,49 @@ public final class NdefRecord implements Parcelable {
public static final short TNF_RESERVED = 0x07;
/**
- * RTD Text type. For use with TNF_WELL_KNOWN.
+ * RTD Text type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_TEXT = {0x54}; // "T"
/**
- * RTD URI type. For use with TNF_WELL_KNOWN.
+ * RTD URI type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_URI = {0x55}; // "U"
/**
- * RTD Smart Poster type. For use with TNF_WELL_KNOWN.
+ * RTD Smart Poster type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_SMART_POSTER = {0x53, 0x70}; // "Sp"
/**
- * RTD Alternative Carrier type. For use with TNF_WELL_KNOWN.
+ * RTD Alternative Carrier type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63}; // "ac"
/**
- * RTD Handover Carrier type. For use with TNF_WELL_KNOWN.
+ * RTD Handover Carrier type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63}; // "Hc"
/**
- * RTD Handover Request type. For use with TNF_WELL_KNOWN.
+ * RTD Handover Request type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72}; // "Hr"
/**
- * RTD Handover Select type. For use with TNF_WELL_KNOWN.
+ * RTD Handover Select type. For use with {@literal TNF_WELL_KNOWN}.
+ * @see #TNF_WELL_KNOWN
*/
public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs"
/**
- * RTD Android app type. For use with TNF_EXTERNAL.
+ * RTD Android app type. For use with {@literal TNF_EXTERNAL}.
* <p>
* The payload of a record with type RTD_ANDROID_APP
* should be the package name identifying an application.
@@ -161,8 +229,7 @@ public final class NdefRecord implements Parcelable {
private static final byte FLAG_IL = (byte) 0x08;
/**
- * NFC Forum "URI Record Type Definition"
- *
+ * NFC Forum "URI Record Type Definition"<p>
* This is a mapping of "URI Identifier Codes" to URI string prefixes,
* per section 3.2.2 of the NFC Forum URI Record Type Definition document.
*/
@@ -204,84 +271,292 @@ public final class NdefRecord implements Parcelable {
"urn:epc:", // 0x22
};
- private final byte mFlags;
+ private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20); // 10 MB payload limit
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
private final short mTnf;
private final byte[] mType;
private final byte[] mId;
private final byte[] mPayload;
/**
- * Construct an NDEF Record.
+ * Create a new Android Application Record (AAR).
+ * <p>
+ * This record indicates to other Android devices the package
+ * that should be used to handle the entire NDEF message.
+ * You can embed this record anywhere into your message
+ * to ensure that the intended package receives the message.
+ * <p>
+ * When an Android device dispatches an {@link NdefMessage}
+ * containing one or more Android application records,
+ * the applications contained in those records will be the
+ * preferred target for the {@link NfcAdapter#ACTION_NDEF_DISCOVERED}
+ * intent, in the order in which they appear in the message.
+ * This dispatch behavior was first added to Android in
+ * Ice Cream Sandwich.
* <p>
- * Applications should not attempt to manually chunk NDEF Records - the
- * implementation of android.nfc will automatically chunk an NDEF Record
- * when necessary (and only present a single logical NDEF Record to the
- * application). So applications should not use TNF_UNCHANGED.
+ * If none of the applications have a are installed on the device,
+ * a Market link will be opened to the first application.
+ * <p>
+ * Note that Android application records do not overrule
+ * applications that have called
+ * {@link NfcAdapter#enableForegroundDispatch}.
*
- * @param tnf a 3-bit TNF constant
- * @param type byte array, containing zero to 255 bytes, must not be null
- * @param id byte array, containing zero to 255 bytes, must not be null
- * @param payload byte array, containing zero to (2 ** 32 - 1) bytes,
- * must not be null
+ * @param packageName Android package name
+ * @return Android application NDEF record
*/
- public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) {
- /* New NDEF records created by applications will have FLAG_MB|FLAG_ME
- * set by default; when multiple records are stored in a
- * {@link NdefMessage}, these flags will be corrected when the {@link NdefMessage}
- * is serialized to bytes.
- */
- this(tnf, type, id, payload, (byte)(FLAG_MB|FLAG_ME));
+ public static NdefRecord createApplicationRecord(String packageName) {
+ if (packageName == null) throw new NullPointerException("packageName is null");
+ if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty");
+
+ return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null,
+ packageName.getBytes(Charsets.UTF_8));
}
/**
- * @hide
+ * Create a new NDEF Record containing a URI.<p>
+ * Use this method to encode a URI (or URL) into an NDEF Record.<p>
+ * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN}
+ * and {@link #RTD_URI}. This is the most efficient encoding
+ * of a URI into NDEF.<p>
+ * The uri parameter will be normalized with
+ * {@link Uri#normalize} to set the scheme to lower case to
+ * follow Android best practices for intent filtering.
+ * However the unchecked exception
+ * {@link IllegalArgumentException} may be thrown if the uri
+ * parameter has serious problems, for example if it is empty, so always
+ * catch this exception if you are passing user-generated data into this
+ * method.<p>
+ *
+ * Reference specification: NFCForum-TS-RTD_URI_1.0
+ *
+ * @param uri URI to encode.
+ * @return an NDEF Record containing the URI
+ * @throws IllegalArugmentException if the uri is empty or invalid
*/
- /*package*/ NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload, byte flags) {
- /* check arguments */
- if ((type == null) || (id == null) || (payload == null)) {
- throw new IllegalArgumentException("Illegal null argument");
- }
+ public static NdefRecord createUri(Uri uri) {
+ if (uri == null) throw new NullPointerException("uri is null");
+
+ uri = uri.normalize();
+ String uriString = uri.toString();
+ if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty");
- if (tnf < 0 || tnf > 0x07) {
- throw new IllegalArgumentException("TNF out of range " + tnf);
+ byte prefix = 0;
+ for (int i = 1; i < URI_PREFIX_MAP.length; i++) {
+ if (uriString.startsWith(URI_PREFIX_MAP[i])) {
+ prefix = (byte) i;
+ uriString = uriString.substring(URI_PREFIX_MAP[i].length());
+ break;
+ }
}
+ byte[] uriBytes = uriString.getBytes(Charsets.UTF_8);
+ byte[] recordBytes = new byte[uriBytes.length + 1];
+ recordBytes[0] = prefix;
+ System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length);
+ return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes);
+ }
- /* Determine if it is a short record */
- if(payload.length < 0xFF) {
- flags |= FLAG_SR;
+ /**
+ * Create a new NDEF Record containing a URI.<p>
+ * Use this method to encode a URI (or URL) into an NDEF Record.<p>
+ * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN}
+ * and {@link #RTD_URI}. This is the most efficient encoding
+ * of a URI into NDEF.<p>
+ * The uriString parameter will be normalized with
+ * {@link Uri#normalize} to set the scheme to lower case to
+ * follow Android best practices for intent filtering.
+ * However the unchecked exception
+ * {@link IllegalArgumentException} may be thrown if the uriString
+ * parameter has serious problems, for example if it is empty, so always
+ * catch this exception if you are passing user-generated data into this
+ * method.<p>
+ *
+ * Reference specification: NFCForum-TS-RTD_URI_1.0
+ *
+ * @param uriString string URI to encode.
+ * @return an NDEF Record containing the URI
+ * @throws IllegalArugmentException if the uriString is empty or invalid
+ */
+ public static NdefRecord createUri(String uriString) {
+ return createUri(Uri.parse(uriString));
+ }
+
+ /**
+ * Create a new NDEF Record containing MIME data.<p>
+ * Use this method to encode MIME-typed data into an NDEF Record,
+ * such as "text/plain", or "image/jpeg".<p>
+ * The mimeType parameter will be normalized with
+ * {@link Intent#normalizeMimeType} to follow Android best
+ * practices for intent filtering, for example to force lower-case.
+ * However the unchecked exception
+ * {@link IllegalArgumentException} may be thrown
+ * if the mimeType parameter has serious problems,
+ * for example if it is empty, so always catch this
+ * exception if you are passing user-generated data into this method.
+ * <p>
+ * For efficiency, This method might not make an internal copy of the
+ * mimeData byte array, so take care not
+ * to modify the mimeData byte array while still using the returned
+ * NdefRecord.
+ *
+ * @param mimeType a valid MIME type
+ * @param mimeData MIME data as bytes
+ * @return an NDEF Record containing the MIME-typed data
+ * @throws IllegalArugmentException if the mimeType is empty or invalid
+ *
+ */
+ public static NdefRecord createMime(String mimeType, byte[] mimeData) {
+ if (mimeType == null) throw new NullPointerException("mimeType is null");
+
+ // We only do basic MIME type validation: trying to follow the
+ // RFCs strictly only ends in tears, since there are lots of MIME
+ // types in common use that are not strictly valid as per RFC rules
+ mimeType = Intent.normalizeMimeType(mimeType);
+ if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty");
+ int slashIndex = mimeType.indexOf('/');
+ if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type");
+ if (slashIndex == mimeType.length() - 1) {
+ throw new IllegalArgumentException("mimeType must have minor type");
}
+ // missing '/' is allowed
- /* Determine if an id is present */
- if(id.length != 0) {
- flags |= FLAG_IL;
+ // MIME RFCs suggest ASCII encoding for content-type
+ byte[] typeBytes = mimeType.getBytes(Charsets.US_ASCII);
+ return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData);
+ }
+
+ /**
+ * Create a new NDEF Record containing external (application-specific) data.<p>
+ * Use this method to encode application specific data into an NDEF Record.
+ * The data is typed by a domain name (usually your Android package name) and
+ * a domain-specific type. This data is packaged into a "NFC Forum External
+ * Type" NDEF Record.<p>
+ * NFC Forum requires that the domain and type used in an external record
+ * are treated as case insensitive, however Android intent filtering is
+ * always case sensitive. So this method will force the domain and type to
+ * lower-case before creating the NDEF Record.<p>
+ * The unchecked exception {@link IllegalArgumentException} will be thrown
+ * if the domain and type have serious problems, for example if either field
+ * is empty, so always catch this
+ * exception if you are passing user-generated data into this method.<p>
+ * There are no such restrictions on the payload data.<p>
+ * For efficiency, This method might not make an internal copy of the
+ * data byte array, so take care not
+ * to modify the data byte array while still using the returned
+ * NdefRecord.
+ *
+ * Reference specification: NFCForum-TS-RTD_1.0
+ * @param domain domain-name of issuing organization
+ * @param type domain-specific type of data
+ * @param data payload as bytes
+ * @throws IllegalArugmentException if either domain or type are empty or invalid
+ */
+ public static NdefRecord createExternal(String domain, String type, byte[] data) {
+ if (domain == null) throw new NullPointerException("domain is null");
+ if (type == null) throw new NullPointerException("type is null");
+
+ domain = domain.trim().toLowerCase(Locale.US);
+ type = type.trim().toLowerCase(Locale.US);
+
+ if (domain.length() == 0) throw new IllegalArgumentException("domain is empty");
+ if (type.length() == 0) throw new IllegalArgumentException("type is empty");
+
+ byte[] byteDomain = domain.getBytes(Charsets.UTF_8);
+ byte[] byteType = type.getBytes(Charsets.UTF_8);
+ byte[] b = new byte[byteDomain.length + 1 + byteType.length];
+ System.arraycopy(byteDomain, 0, b, 0, byteDomain.length);
+ b[byteDomain.length] = ':';
+ System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length);
+
+ return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data);
+ }
+
+ /**
+ * Construct an NDEF Record from its component fields.<p>
+ * Recommend to use helpers such as {#createUri} or
+ * {{@link #createExternal} where possible, since they perform
+ * stricter validation that the record is correctly formatted
+ * as per NDEF specifications. However if you know what you are
+ * doing then this constructor offers the most flexibility.<p>
+ * An {@link NdefRecord} represents a logical (complete)
+ * record, and cannot represent NDEF Record chunks.<p>
+ * Basic validation of the tnf, type, id and payload is performed
+ * as per the following rules:
+ * <ul>
+ * <li>The tnf paramter must be a 3-bit value.</li>
+ * <li>Records with a tnf of {@link #TNF_EMPTY} cannot have a type,
+ * id or payload.</li>
+ * <li>Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07}
+ * cannot have a type.</li>
+ * <li>Records with a tnf of {@link #TNF_UNCHANGED} are not allowed
+ * since this class only represents complete (unchunked) records.</li>
+ * </ul>
+ * This minimal validation is specified by
+ * NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).<p>
+ * If any of the above validation
+ * steps fail then {@link IllegalArgumentException} is thrown.<p>
+ * Deep inspection of the type, id and payload fields is not
+ * performed, so it is possible to create NDEF Records
+ * that conform to section 3.2.6
+ * but fail other more strict NDEF specification requirements. For
+ * example, the payload may be invalid given the tnf and type.
+ * <p>
+ * To omit a type, id or payload field, set the parameter to an
+ * empty byte array or null.
+ *
+ * @param tnf a 3-bit TNF constant
+ * @param type byte array, containing zero to 255 bytes, or null
+ * @param id byte array, containing zero to 255 bytes, or null
+ * @param payload byte array, containing zero to (2 ** 32 - 1) bytes,
+ * or null
+ * @throws IllegalArugmentException if a valid record cannot be created
+ */
+ public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) {
+ /* convert nulls */
+ if (type == null) type = EMPTY_BYTE_ARRAY;
+ if (id == null) id = EMPTY_BYTE_ARRAY;
+ if (payload == null) payload = EMPTY_BYTE_ARRAY;
+
+ String message = validateTnf(tnf, type, id, payload);
+ if (message != null) {
+ throw new IllegalArgumentException(message);
}
- mFlags = flags;
mTnf = tnf;
- mType = type.clone();
- mId = id.clone();
- mPayload = payload.clone();
+ mType = type;
+ mId = id;
+ mPayload = payload;
}
/**
- * Construct an NDEF Record from raw bytes.
- * <p>
- * Validation is performed to make sure the header is valid, and that
- * the id, type and payload sizes appear to be valid.
+ * Construct an NDEF Record from raw bytes.<p>
+ * This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])}
+ * instead. This is because it does not make sense to parse a record:
+ * the NDEF binary format is only defined for a message, and the
+ * record flags MB and ME do not make sense outside of the context of
+ * an entire message.<p>
+ * This implementation will attempt to parse a single record by ignoring
+ * the MB and ME flags, and otherwise following the rules of
+ * {@link NdefMessage#NdefMessage(byte[])}.<p>
*
- * @throws FormatException if the data is not a valid NDEF record
+ * @param data raw bytes to parse
+ * @throws FormatException if the data cannot be parsed into a valid record
+ * @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead.
*/
+ @Deprecated
public NdefRecord(byte[] data) throws FormatException {
- /* Prevent compiler to complain about unassigned final fields */
- mFlags = 0;
- mTnf = 0;
- mType = null;
- mId = null;
- mPayload = null;
- /* Perform actual parsing */
- if (parseNdefRecord(data) == -1) {
- throw new FormatException("Error while parsing NDEF record");
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ NdefRecord[] rs = parse(buffer, true);
+
+ if (buffer.remaining() > 0) {
+ throw new FormatException("data too long");
}
+
+ mTnf = rs[0].mTnf;
+ mType = rs[0].mType;
+ mId = rs[0].mId;
+ mPayload = rs[0].mPayload;
}
/**
@@ -298,6 +573,9 @@ public final class NdefRecord implements Parcelable {
* <p>
* This should be used in conjunction with the TNF field to determine the
* payload format.
+ * <p>
+ * Returns an empty byte array if this record
+ * does not have a type field.
*/
public byte[] getType() {
return mType.clone();
@@ -305,6 +583,9 @@ public final class NdefRecord implements Parcelable {
/**
* Returns the variable length ID.
+ * <p>
+ * Returns an empty byte array if this record
+ * does not have an id field.
*/
public byte[] getId() {
return mId.clone();
@@ -312,125 +593,349 @@ public final class NdefRecord implements Parcelable {
/**
* Returns the variable length payload.
+ * <p>
+ * Returns an empty byte array if this record
+ * does not have a payload field.
*/
public byte[] getPayload() {
return mPayload.clone();
}
/**
- * Helper to return the NdefRecord as a URI.
- * TODO: Consider making a member method instead of static
- * TODO: Consider more validation that this is a URI record
- * TODO: Make a public API
- * @hide
+ * Return this NDEF Record as a byte array.<p>
+ * This method is deprecated, use {@link NdefMessage#toByteArray}
+ * instead. This is because the NDEF binary format is not defined for
+ * a record outside of the context of a message: the MB and ME flags
+ * cannot be set without knowing the location inside a message.<p>
+ * This implementation will attempt to serialize a single record by
+ * always setting the MB and ME flags (in other words, assume this
+ * is a single-record NDEF Message).<p>
+ *
+ * @deprecated use {@link NdefMessage#toByteArray()} instead
*/
- public static Uri parseWellKnownUriRecord(NdefRecord record) throws FormatException {
- byte[] payload = record.getPayload();
- if (payload.length < 2) {
- throw new FormatException("Payload is not a valid URI (missing prefix)");
- }
+ @Deprecated
+ public byte[] toByteArray() {
+ ByteBuffer buffer = ByteBuffer.allocate(getByteLength());
+ writeToByteBuffer(buffer, true, true);
+ return buffer.array();
+ }
- /*
- * payload[0] contains the URI Identifier Code, per the
- * NFC Forum "URI Record Type Definition" section 3.2.2.
- *
- * payload[1]...payload[payload.length - 1] contains the rest of
- * the URI.
- */
- int prefixIndex = (payload[0] & 0xff);
- if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) {
- throw new FormatException("Payload is not a valid URI (invalid prefix)");
+ /**
+ * Map this record to a MIME type, or return null if it cannot be mapped.<p>
+ * Currently this method considers all {@link #TNF_MIME_MEDIA} records to
+ * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as
+ * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string
+ * is returned, otherwise null is returned.<p>
+ * This method does not perform validation that the MIME type is
+ * actually valid. It always attempts to
+ * return a string containing the type if this is a MIME record.<p>
+ * The returned MIME type will by normalized to lower-case using
+ * {@link Intent#normalizeMimeType}.<p>
+ * The MIME payload can be obtained using {@link #getPayload}.
+ *
+ * @return MIME type as a string, or null if this is not a MIME record
+ */
+ public String toMimeType() {
+ switch (mTnf) {
+ case NdefRecord.TNF_WELL_KNOWN:
+ if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) {
+ return "text/plain";
+ }
+ break;
+ case NdefRecord.TNF_MIME_MEDIA:
+ String mimeType = new String(mType, Charsets.US_ASCII);
+ return Intent.normalizeMimeType(mimeType);
}
- String prefix = URI_PREFIX_MAP[prefixIndex];
- byte[] fullUri = concat(prefix.getBytes(Charsets.UTF_8),
- Arrays.copyOfRange(payload, 1, payload.length));
- return Uri.parse(new String(fullUri, Charsets.UTF_8));
+ return null;
}
/**
- * Creates an Android application NDEF record.
- * <p>
- * This record indicates to other Android devices the package
- * that should be used to handle the rest of the NDEF message.
- * You can embed this record anywhere into your NDEF message
- * to ensure that the intended package receives the message.
- * <p>
- * When an Android device dispatches an {@link NdefMessage}
- * containing one or more Android application records,
- * the applications contained in those records will be the
- * preferred target for the NDEF_DISCOVERED intent, in
- * the order in which they appear in the {@link NdefMessage}.
- * This dispatch behavior was first added to Android in
- * Ice Cream Sandwich.
- * <p>
- * If none of the applications are installed on the device,
- * a Market link will be opened to the first application.
- * <p>
- * Note that Android application records do not overrule
- * applications that have called
- * {@link NfcAdapter#enableForegroundDispatch}.
+ * Map this record to a URI, or return null if it cannot be mapped.<p>
+ * Currently this method considers the following to be URI records:
+ * <ul>
+ * <li>{@link #TNF_ABSOLUTE_URI} records.</li>
+ * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.</li>
+ * <li>{@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER}
+ * and containing a URI record in the NDEF message nested in the payload.
+ * </li>
+ * <li>{@link #TNF_EXTERNAL_TYPE} records.</li>
+ * </ul>
+ * If this is not a URI record by the above rules, then null is returned.<p>
+ * This method does not perform validation that the URI is
+ * actually valid: it always attempts to create and return a URI if
+ * this record appears to be a URI record by the above rules.<p>
+ * The returned URI will be normalized to have a lower case scheme
+ * using {@link Uri#normalize}.<p>
*
- * @param packageName Android package name
- * @return Android application NDEF record
+ * @return URI, or null if this is not a URI record
*/
- public static NdefRecord createApplicationRecord(String packageName) {
- return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, new byte[] {},
- packageName.getBytes(Charsets.US_ASCII));
+ public Uri toUri() {
+ return toUri(false);
+ }
+
+ private Uri toUri(boolean inSmartPoster) {
+ switch (mTnf) {
+ case TNF_WELL_KNOWN:
+ if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) {
+ try {
+ // check payload for a nested NDEF Message containing a URI
+ NdefMessage nestedMessage = new NdefMessage(mPayload);
+ for (NdefRecord nestedRecord : nestedMessage.getRecords()) {
+ Uri uri = nestedRecord.toUri(true);
+ if (uri != null) {
+ return uri;
+ }
+ }
+ } catch (FormatException e) { }
+ } else if (Arrays.equals(mType, RTD_URI)) {
+ return parseWktUri().normalize();
+ }
+ break;
+
+ case TNF_ABSOLUTE_URI:
+ Uri uri = Uri.parse(new String(mType, Charsets.UTF_8));
+ return uri.normalize();
+
+ case TNF_EXTERNAL_TYPE:
+ if (inSmartPoster) {
+ break;
+ }
+ return Uri.parse("vnd.android.nfc://ext/" + new String(mType, Charsets.US_ASCII));
+ }
+ return null;
}
/**
- * Creates an NDEF record of well known type URI.
+ * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records.
+ * @return complete URI, or null if invalid
*/
- public static NdefRecord createUri(Uri uri) {
- return createUri(uri.toString());
+ private Uri parseWktUri() {
+ if (mPayload.length < 2) {
+ return null;
+ }
+
+ // payload[0] contains the URI Identifier Code, as per
+ // NFC Forum "URI Record Type Definition" section 3.2.2.
+ int prefixIndex = (mPayload[0] & (byte)0xFF);
+ if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) {
+ return null;
+ }
+ String prefix = URI_PREFIX_MAP[prefixIndex];
+ String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length),
+ Charsets.UTF_8);
+ return Uri.parse(prefix + suffix);
}
/**
- * Creates an NDEF record of well known type URI.
+ * Main record parsing method.<p>
+ * Expects NdefMessage to begin immediately, allows trailing data.<p>
+ * Currently has strict validation of all fields as per NDEF 1.0
+ * specification section 2.5. We will attempt to keep this as strict as
+ * possible to encourage well-formatted NDEF.<p>
+ * Always returns 1 or more NdefRecord's, or throws FormatException.
+ *
+ * @param buffer ByteBuffer to read from
+ * @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record
+ * @return one or more records
+ * @throws FormatException on any parsing error
*/
- public static NdefRecord createUri(String uriString) {
- byte prefix = 0x0;
- for (int i = 1; i < URI_PREFIX_MAP.length; i++) {
- if (uriString.startsWith(URI_PREFIX_MAP[i])) {
- prefix = (byte) i;
- uriString = uriString.substring(URI_PREFIX_MAP[i].length());
- break;
+ static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException {
+ List<NdefRecord> records = new ArrayList<NdefRecord>();
+
+ try {
+ byte[] type = null;
+ byte[] id = null;
+ byte[] payload = null;
+ ArrayList<byte[]> chunks = new ArrayList<byte[]>();
+ boolean inChunk = false;
+ short chunkTnf = -1;
+ boolean me = false;
+
+ while (!me) {
+ byte flag = buffer.get();
+
+ boolean mb = (flag & NdefRecord.FLAG_MB) != 0;
+ me = (flag & NdefRecord.FLAG_ME) != 0;
+ boolean cf = (flag & NdefRecord.FLAG_CF) != 0;
+ boolean sr = (flag & NdefRecord.FLAG_SR) != 0;
+ boolean il = (flag & NdefRecord.FLAG_IL) != 0;
+ short tnf = (short)(flag & 0x07);
+
+ if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) {
+ throw new FormatException("expected MB flag");
+ } else if (mb && records.size() != 0 && !ignoreMbMe) {
+ throw new FormatException("unexpected MB flag");
+ } else if (inChunk && il) {
+ throw new FormatException("unexpected IL flag in non-leading chunk");
+ } else if (cf && me) {
+ throw new FormatException("unexpected ME flag in non-trailing chunk");
+ } else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) {
+ throw new FormatException("expected TNF_UNCHANGED in non-leading chunk");
+ } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) {
+ throw new FormatException("" +
+ "unexpected TNF_UNCHANGED in first chunk or unchunked record");
+ }
+
+ int typeLength = buffer.get() & 0xFF;
+ long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL);
+ int idLength = il ? (buffer.get() & 0xFF) : 0;
+
+ if (inChunk && typeLength != 0) {
+ throw new FormatException("expected zero-length type in non-leading chunk");
+ }
+
+ if (!inChunk) {
+ type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY);
+ id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY);
+ buffer.get(type);
+ buffer.get(id);
+ }
+
+ ensureSanePayloadSize(payloadLength);
+ payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY);
+ buffer.get(payload);
+
+ if (cf && !inChunk) {
+ // first chunk
+ chunks.clear();
+ chunkTnf = tnf;
+ }
+ if (cf || inChunk) {
+ // any chunk
+ chunks.add(payload);
+ }
+ if (!cf && inChunk) {
+ // last chunk, flatten the payload
+ payloadLength = 0;
+ for (byte[] p : chunks) {
+ payloadLength += p.length;
+ }
+ ensureSanePayloadSize(payloadLength);
+ payload = new byte[(int)payloadLength];
+ int i = 0;
+ for (byte[] p : chunks) {
+ System.arraycopy(p, 0, payload, i, p.length);
+ i += p.length;
+ }
+ tnf = chunkTnf;
+ }
+ if (cf) {
+ // more chunks to come
+ inChunk = true;
+ continue;
+ } else {
+ inChunk = false;
+ }
+
+ String error = validateTnf(tnf, type, id, payload);
+ if (error != null) {
+ throw new FormatException(error);
+ }
+ records.add(new NdefRecord(tnf, type, id, payload));
+ if (ignoreMbMe) { // for parsing a single NdefRecord
+ break;
+ }
}
+ } catch (BufferUnderflowException e) {
+ throw new FormatException("expected more data", e);
}
- byte[] uriBytes = uriString.getBytes(Charsets.UTF_8);
- byte[] recordBytes = new byte[uriBytes.length + 1];
- recordBytes[0] = prefix;
- System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length);
- return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, new byte[0], recordBytes);
+ return records.toArray(new NdefRecord[records.size()]);
}
- private static byte[] concat(byte[]... arrays) {
- int length = 0;
- for (byte[] array : arrays) {
- length += array.length;
+ private static void ensureSanePayloadSize(long size) throws FormatException {
+ if (size > MAX_PAYLOAD_SIZE) {
+ throw new FormatException(
+ "payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE);
}
- byte[] result = new byte[length];
- int pos = 0;
- for (byte[] array : arrays) {
- System.arraycopy(array, 0, result, pos, array.length);
- pos += array.length;
+ }
+
+ /**
+ * Perform simple validation that the tnf is valid.<p>
+ * Validates the requirements of NFCForum-TS-NDEF_1.0 section
+ * 3.2.6 (Type Name Format). This just validates that the tnf
+ * is valid, and that the relevant type, id and payload
+ * fields are present (or empty) for this tnf. It does not
+ * perform any deep inspection of the type, id and payload fields.<p>
+ * Also does not allow TNF_UNCHANGED since this class is only used
+ * to present logical (unchunked) records.
+ *
+ * @return null if valid, or a string error if invalid.
+ */
+ static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) {
+ switch (tnf) {
+ case TNF_EMPTY:
+ if (type.length != 0 || id.length != 0 || payload.length != 0) {
+ return "unexpected data in TNF_EMPTY record";
+ }
+ return null;
+ case TNF_WELL_KNOWN:
+ case TNF_MIME_MEDIA:
+ case TNF_ABSOLUTE_URI:
+ case TNF_EXTERNAL_TYPE:
+ return null;
+ case TNF_UNKNOWN:
+ case TNF_RESERVED:
+ if (type.length != 0) {
+ return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record";
+ }
+ return null;
+ case TNF_UNCHANGED:
+ return "unexpected TNF_UNCHANGED in first chunk or logical record";
+ default:
+ return String.format("unexpected tnf value: 0x%02x", tnf);
}
- return result;
}
/**
- * Returns this entire NDEF Record as a byte array.
+ * Serialize record for network transmission.<p>
+ * Uses specified MB and ME flags.<p>
+ * Does not chunk records.
*/
- public byte[] toByteArray() {
- return generate(mFlags, mTnf, mType, mId, mPayload);
+ void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) {
+ boolean sr = mPayload.length < 256;
+ boolean il = mId.length > 0;
+
+ byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) |
+ (sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf);
+ buffer.put(flags);
+
+ buffer.put((byte)mType.length);
+ if (sr) {
+ buffer.put((byte)mPayload.length);
+ } else {
+ buffer.putInt(mPayload.length);
+ }
+ if (il) {
+ buffer.put((byte)mId.length);
+ }
+
+ buffer.put(mType);
+ buffer.put(mId);
+ buffer.put(mPayload);
+ }
+
+ /**
+ * Get byte length of serialized record.
+ */
+ int getByteLength() {
+ int length = 3 + mType.length + mId.length + mPayload.length;
+
+ boolean sr = mPayload.length < 256;
+ boolean il = mId.length > 0;
+
+ if (!sr) length += 3;
+ if (il) length += 1;
+
+ return length;
}
+ @Override
public int describeContents() {
return 0;
}
+ @Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mFlags);
dest.writeInt(mTnf);
dest.writeInt(mType.length);
dest.writeByteArray(mType);
@@ -442,8 +947,8 @@ public final class NdefRecord implements Parcelable {
public static final Parcelable.Creator<NdefRecord> CREATOR =
new Parcelable.Creator<NdefRecord>() {
+ @Override
public NdefRecord createFromParcel(Parcel in) {
- byte flags = (byte)in.readInt();
short tnf = (short)in.readInt();
int typeLength = in.readInt();
byte[] type = new byte[typeLength];
@@ -455,13 +960,55 @@ public final class NdefRecord implements Parcelable {
byte[] payload = new byte[payloadLength];
in.readByteArray(payload);
- return new NdefRecord(tnf, type, id, payload, flags);
+ return new NdefRecord(tnf, type, id, payload);
}
+ @Override
public NdefRecord[] newArray(int size) {
return new NdefRecord[size];
}
};
- private native int parseNdefRecord(byte[] data);
- private native byte[] generate(short flags, short tnf, byte[] type, byte[] id, byte[] data);
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(mId);
+ result = prime * result + Arrays.hashCode(mPayload);
+ result = prime * result + mTnf;
+ result = prime * result + Arrays.hashCode(mType);
+ return result;
+ }
+
+ /**
+ * Returns true if the specified NDEF Record contains
+ * identical tnf, type, id and payload fields.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ NdefRecord other = (NdefRecord) obj;
+ if (!Arrays.equals(mId, other.mId)) return false;
+ if (!Arrays.equals(mPayload, other.mPayload)) return false;
+ if (mTnf != other.mTnf) return false;
+ return Arrays.equals(mType, other.mType);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf));
+ if (mType.length > 0) b.append(" type=").append(bytesToString(mType));
+ if (mId.length > 0) b.append(" id=").append(bytesToString(mId));
+ if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload));
+ return b.toString();
+ }
+
+ private static StringBuilder bytesToString(byte[] bs) {
+ StringBuilder s = new StringBuilder();
+ for (byte b : bs) {
+ s.append(String.format("%02X", b));
+ }
+ return s;
+ }
}
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index a903a5f..5176857 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -66,6 +66,9 @@ public final class NfcAdapter {
* <p>If the tag has an NDEF payload this intent is started before
* {@link #ACTION_TECH_DISCOVERED}. If any activities respond to this intent neither
* {@link #ACTION_TECH_DISCOVERED} or {@link #ACTION_TAG_DISCOVERED} will be started.
+ *
+ * <p>The MIME type or data URI of this intent are normalized before dispatch -
+ * so that MIME, URI scheme and URI host are always lower-case.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED";
@@ -151,9 +154,13 @@ public final class NfcAdapter {
public static final String EXTRA_TAG = "android.nfc.extra.TAG";
/**
- * Optional extra containing an array of {@link NdefMessage} present on the discovered tag for
- * the {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED}, and
- * {@link #ACTION_TAG_DISCOVERED} intents.
+ * Extra containing an array of {@link NdefMessage} present on the discovered tag.<p>
+ * This extra is mandatory for {@link #ACTION_NDEF_DISCOVERED} intents,
+ * and optional for {@link #ACTION_TECH_DISCOVERED}, and
+ * {@link #ACTION_TAG_DISCOVERED} intents.<p>
+ * When this extra is present there will always be at least one
+ * {@link NdefMessage} element. Most NDEF tags have only one NDEF message,
+ * but we use an array for future compatibility.
*/
public static final String EXTRA_NDEF_MESSAGES = "android.nfc.extra.NDEF_MESSAGES";
@@ -363,8 +370,11 @@ public final class NfcAdapter {
throw new IllegalArgumentException("context cannot be null");
}
context = context.getApplicationContext();
- /* use getSystemService() instead of just instantiating to take
- * advantage of the context's cached NfcManager & NfcAdapter */
+ if (context == null) {
+ throw new IllegalArgumentException(
+ "context not associated with any application (using a mock context?)");
+ }
+ /* use getSystemService() for consistency */
NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE);
if (manager == null) {
// NFC not available
@@ -379,9 +389,14 @@ public final class NfcAdapter {
* for many NFC API methods. Those methods will fail when called on an NfcAdapter
* object created from this method.<p>
* @deprecated use {@link #getDefaultAdapter(Context)}
+ * @hide
*/
@Deprecated
public static NfcAdapter getDefaultAdapter() {
+ // introduced in API version 9 (GB 2.3)
+ // deprecated in API version 10 (GB 2.3.3)
+ // removed from public API in version 16 (ICS MR2)
+ // should maintain as a hidden API for binary compatibility for a little longer
Log.w(TAG, "WARNING: NfcAdapter.getDefaultAdapter() is deprecated, use " +
"NfcAdapter.getDefaultAdapter(Context) instead", new Exception());
@@ -448,11 +463,13 @@ public final class NfcAdapter {
/**
* Return true if this NFC Adapter has any features enabled.
*
- * <p>Application may use this as a helper to suggest that the user
- * should turn on NFC in Settings.
* <p>If this method returns false, the NFC hardware is guaranteed not to
- * generate or respond to any NFC transactions.
+ * generate or respond to any NFC communication over its NFC radio.
+ * <p>Applications can use this to check if NFC is enabled. Applications
+ * can request Settings UI allowing the user to toggle NFC using:
+ * <p><pre>startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))</pre>
*
+ * @see android.provider.Settings#ACTION_NFC_SETTINGS
* @return true if this NFC Adapter has any features enabled
*/
public boolean isEnabled() {
@@ -793,6 +810,7 @@ public final class NfcAdapter {
* @throws IllegalStateException if the Activity has already been paused
* @deprecated use {@link #setNdefPushMessage} instead
*/
+ @Deprecated
public void disableForegroundNdefPush(Activity activity) {
if (activity == null) {
throw new NullPointerException();
@@ -804,61 +822,6 @@ public final class NfcAdapter {
}
/**
- * TODO: Remove this once pre-built apk's (Maps, Youtube etc) are updated
- * @deprecated use {@link CreateNdefMessageCallback} or {@link OnNdefPushCompleteCallback}
- * @hide
- */
- @Deprecated
- public interface NdefPushCallback {
- /**
- * @deprecated use {@link CreateNdefMessageCallback} instead
- */
- @Deprecated
- NdefMessage createMessage();
- /**
- * @deprecated use{@link OnNdefPushCompleteCallback} instead
- */
- @Deprecated
- void onMessagePushed();
- }
-
- /**
- * TODO: Remove this
- * Converts new callbacks to old callbacks.
- */
- static final class LegacyCallbackWrapper implements CreateNdefMessageCallback,
- OnNdefPushCompleteCallback {
- final NdefPushCallback mLegacyCallback;
- LegacyCallbackWrapper(NdefPushCallback legacyCallback) {
- mLegacyCallback = legacyCallback;
- }
- @Override
- public void onNdefPushComplete(NfcEvent event) {
- mLegacyCallback.onMessagePushed();
- }
- @Override
- public NdefMessage createNdefMessage(NfcEvent event) {
- return mLegacyCallback.createMessage();
- }
- }
-
- /**
- * TODO: Remove this once pre-built apk's (Maps, Youtube etc) are updated
- * @deprecated use {@link #setNdefPushMessageCallback} instead
- * @hide
- */
- @Deprecated
- public void enableForegroundNdefPush(Activity activity, final NdefPushCallback callback) {
- if (activity == null || callback == null) {
- throw new NullPointerException();
- }
- enforceResumed(activity);
- LegacyCallbackWrapper callbackWrapper = new LegacyCallbackWrapper(callback);
- mNfcActivityManager.setNdefPushMessageCallback(activity, callbackWrapper);
- mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callbackWrapper);
- }
-
- /**
* Enable NDEF Push feature.
* <p>This API is for the Settings application.
* @hide
@@ -887,16 +850,28 @@ public final class NfcAdapter {
}
/**
- * Return true if NDEF Push feature is enabled.
- * <p>This function can return true even if NFC is currently turned-off.
- * This indicates that NDEF Push is not currently active, but it has
- * been requested by the user and will be active as soon as NFC is turned
- * on.
- * <p>If you want to check if NDEF PUsh sharing is currently active, use
- * <code>{@link #isEnabled()} && {@link #isNdefPushEnabled()}</code>
+ * Return true if the NDEF Push (Android Beam) feature is enabled.
+ * <p>This function will return true only if both NFC is enabled, and the
+ * NDEF Push feature is enabled.
+ * <p>Note that if NFC is enabled but NDEF Push is disabled then this
+ * device can still <i>receive</i> NDEF messages, it just cannot send them.
+ * <p>Applications cannot directly toggle the NDEF Push feature, but they
+ * can request Settings UI allowing the user to toggle NDEF Push using
+ * <code>startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS))</code>
+ * <p>Example usage in an Activity that requires NDEF Push:
+ * <p><pre>
+ * protected void onResume() {
+ * super.onResume();
+ * if (!nfcAdapter.isEnabled()) {
+ * startActivity(new Intent(Settings.ACTION_NFC_SETTINGS));
+ * } else if (!nfcAdapter.isNdefPushEnabled()) {
+ * startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS));
+ * }
+ * }
+ * </pre>
*
+ * @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS
* @return true if NDEF Push feature is enabled
- * @hide
*/
public boolean isNdefPushEnabled() {
try {
@@ -908,6 +883,24 @@ public final class NfcAdapter {
}
/**
+ * Inject a mock NFC tag.<p>
+ * Used for testing purposes.
+ * <p class="note">Requires the
+ * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission.
+ * @hide
+ */
+ public void dispatch(Tag tag) {
+ if (tag == null) {
+ throw new NullPointerException("tag cannot be null");
+ }
+ try {
+ sService.dispatch(tag);
+ } catch (RemoteException e) {
+ attemptDeadServiceRecovery(e);
+ }
+ }
+
+ /**
* @hide
*/
public INfcAdapterExtras getNfcAdapterExtrasInterface() {
diff --git a/core/java/android/nfc/NfcManager.java b/core/java/android/nfc/NfcManager.java
index ef5c7ba..ea08014 100644
--- a/core/java/android/nfc/NfcManager.java
+++ b/core/java/android/nfc/NfcManager.java
@@ -46,6 +46,10 @@ public final class NfcManager {
public NfcManager(Context context) {
NfcAdapter adapter;
context = context.getApplicationContext();
+ if (context == null) {
+ throw new IllegalArgumentException(
+ "context not associated with any application (using a mock context?)");
+ }
try {
adapter = NfcAdapter.getNfcAdapter(context);
} catch (UnsupportedOperationException e) {
diff --git a/core/java/android/nfc/Tag.java b/core/java/android/nfc/Tag.java
index a73067a..f9b765c 100644
--- a/core/java/android/nfc/Tag.java
+++ b/core/java/android/nfc/Tag.java
@@ -108,14 +108,14 @@ import java.util.Arrays;
* <p>
*/
public final class Tag implements Parcelable {
- /*package*/ final byte[] mId;
- /*package*/ final int[] mTechList;
- /*package*/ final String[] mTechStringList;
- /*package*/ final Bundle[] mTechExtras;
- /*package*/ final int mServiceHandle; // for use by NFC service, 0 indicates a mock
- /*package*/ final INfcTag mTagService;
+ final byte[] mId;
+ final int[] mTechList;
+ final String[] mTechStringList;
+ final Bundle[] mTechExtras;
+ final int mServiceHandle; // for use by NFC service, 0 indicates a mock
+ final INfcTag mTagService; // interface to NFC service, will be null if mock tag
- /*package*/ int mConnectedTechnology;
+ int mConnectedTechnology;
/**
* Hidden constructor to be used by NFC service and internal classes.
@@ -148,7 +148,7 @@ public final class Tag implements Parcelable {
* @hide
*/
public static Tag createMockTag(byte[] id, int[] techList, Bundle[] techListExtras) {
- // set serviceHandle to 0 to indicate mock tag
+ // set serviceHandle to 0 and tagService to null to indicate mock tag
return new Tag(id, techList, techListExtras, 0, null);
}
@@ -266,6 +266,9 @@ public final class Tag implements Parcelable {
throw new IllegalStateException("Close connection to the technology first!");
}
+ if (mTagService == null) {
+ throw new IOException("Mock tags don't support this operation.");
+ }
try {
Tag newTag = mTagService.rediscover(getServiceHandle());
if (newTag != null) {
diff --git a/core/java/android/nfc/tech/BasicTagTechnology.java b/core/java/android/nfc/tech/BasicTagTechnology.java
index 913ae0e..b6b347c 100644
--- a/core/java/android/nfc/tech/BasicTagTechnology.java
+++ b/core/java/android/nfc/tech/BasicTagTechnology.java
@@ -18,7 +18,6 @@ package android.nfc.tech;
import android.nfc.ErrorCodes;
import android.nfc.Tag;
-import android.nfc.TagLostException;
import android.nfc.TransceiveResult;
import android.os.RemoteException;
import android.util.Log;
@@ -28,12 +27,13 @@ import java.io.IOException;
/**
* A base class for tag technologies that are built on top of transceive().
*/
-/* package */ abstract class BasicTagTechnology implements TagTechnology {
+abstract class BasicTagTechnology implements TagTechnology {
private static final String TAG = "NFC";
- /*package*/ final Tag mTag;
- /*package*/ boolean mIsConnected;
- /*package*/ int mSelectedTechnology;
+ final Tag mTag;
+
+ boolean mIsConnected;
+ int mSelectedTechnology;
BasicTagTechnology(Tag tag, int tech) throws RemoteException {
mTag = tag;
@@ -139,7 +139,7 @@ import java.io.IOException;
}
}
/** Internal transceive */
- /*package*/ byte[] transceive(byte[] data, boolean raw) throws IOException {
+ byte[] transceive(byte[] data, boolean raw) throws IOException {
checkConnected();
try {
diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java
index b266bb6..226e079 100644
--- a/core/java/android/nfc/tech/Ndef.java
+++ b/core/java/android/nfc/tech/Ndef.java
@@ -259,6 +259,9 @@ public final class Ndef extends BasicTagTechnology {
try {
INfcTag tagService = mTag.getTagService();
+ if (tagService == null) {
+ throw new IOException("Mock tags don't support this operation.");
+ }
int serviceHandle = mTag.getServiceHandle();
if (tagService.isNdef(serviceHandle)) {
NdefMessage msg = tagService.ndefRead(serviceHandle);
@@ -303,6 +306,9 @@ public final class Ndef extends BasicTagTechnology {
try {
INfcTag tagService = mTag.getTagService();
+ if (tagService == null) {
+ throw new IOException("Mock tags don't support this operation.");
+ }
int serviceHandle = mTag.getServiceHandle();
if (tagService.isNdef(serviceHandle)) {
int errorCode = tagService.ndefWrite(serviceHandle, msg);
@@ -335,6 +341,9 @@ public final class Ndef extends BasicTagTechnology {
*/
public boolean canMakeReadOnly() {
INfcTag tagService = mTag.getTagService();
+ if (tagService == null) {
+ return false;
+ }
try {
return tagService.canMakeReadOnly(mNdefType);
} catch (RemoteException e) {
@@ -366,6 +375,9 @@ public final class Ndef extends BasicTagTechnology {
try {
INfcTag tagService = mTag.getTagService();
+ if (tagService == null) {
+ return false;
+ }
if (tagService.isNdef(mTag.getServiceHandle())) {
int errorCode = tagService.ndefMakeReadOnly(mTag.getServiceHandle());
switch (errorCode) {
diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java
index 9dea4c4..fd6bed7 100644
--- a/core/java/android/os/AsyncTask.java
+++ b/core/java/android/os/AsyncTask.java
@@ -135,6 +135,8 @@ import java.util.concurrent.atomic.AtomicInteger;
* <p>There are a few threading rules that must be followed for this class to
* work properly:</p>
* <ul>
+ * <li>The AsyncTask class must be loaded on the UI thread. This is done
+ * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
* <li>The task instance must be created on the UI thread.</li>
* <li>{@link #execute} must be invoked on the UI thread.</li>
* <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
@@ -195,6 +197,7 @@ public abstract class AsyncTask<Params, Progress, Result> {
private volatile Status mStatus = Status.PENDING;
+ private final AtomicBoolean mCancelled = new AtomicBoolean();
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
private static class SerialExecutor implements Executor {
@@ -261,6 +264,7 @@ public abstract class AsyncTask<Params, Progress, Result> {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ //noinspection unchecked
return postResult(doInBackground(mParams));
}
};
@@ -269,9 +273,7 @@ public abstract class AsyncTask<Params, Progress, Result> {
@Override
protected void done() {
try {
- final Result result = get();
-
- postResultIfNotInvoked(result);
+ postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
@@ -295,6 +297,7 @@ public abstract class AsyncTask<Params, Progress, Result> {
}
private Result postResult(Result result) {
+ @SuppressWarnings("unchecked")
Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
@@ -411,7 +414,7 @@ public abstract class AsyncTask<Params, Progress, Result> {
* @see #cancel(boolean)
*/
public final boolean isCancelled() {
- return mFuture.isCancelled();
+ return mCancelled.get();
}
/**
@@ -444,6 +447,7 @@ public abstract class AsyncTask<Params, Progress, Result> {
* @see #onCancelled(Object)
*/
public final boolean cancel(boolean mayInterruptIfRunning) {
+ mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index c25ebb7..24569fa 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -337,13 +337,16 @@ public class Binder implements IBinder {
try {
res = onTransact(code, data, reply, flags);
} catch (RemoteException e) {
+ reply.setDataPosition(0);
reply.writeException(e);
res = true;
} catch (RuntimeException e) {
+ reply.setDataPosition(0);
reply.writeException(e);
res = true;
} catch (OutOfMemoryError e) {
RuntimeException re = new RuntimeException("Out of memory", e);
+ reply.setDataPosition(0);
reply.writeException(re);
res = true;
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 88fea91..c106092 100644
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -167,6 +167,8 @@ public class Build {
* medium density normal size screens unless otherwise indicated).
* They can still explicitly specify screen support either way with the
* supports-screens manifest tag.
+ * <li> {@link android.widget.TabHost} will use the new dark tab
+ * background design.
* </ul>
*/
public static final int DONUT = 4;
@@ -208,6 +210,13 @@ public class Build {
/**
* November 2010: Android 2.3
+ *
+ * <p>Applications targeting this or a later release will get these
+ * new changes in behavior:</p>
+ * <ul>
+ * <li> The application's notification icons will be shown on the new
+ * dark status bar background, so must be visible in this situation.
+ * </ul>
*/
public static final int GINGERBREAD = 9;
@@ -224,14 +233,34 @@ public class Build {
* <ul>
* <li> The default theme for applications is now dark holographic:
* {@link android.R.style#Theme_Holo}.
+ * <li> On large screen devices that do not have a physical menu
+ * button, the soft (compatibility) menu is disabled.
* <li> The activity lifecycle has changed slightly as per
* {@link android.app.Activity}.
+ * <li> An application will crash if it does not call through
+ * to the super implementation of its
+ * {@link android.app.Activity#onPause Activity.onPause()} method.
* <li> When an application requires a permission to access one of
* its components (activity, receiver, service, provider), this
* permission is no longer enforced when the application wants to
* access its own component. This means it can require a permission
* on a component that it does not itself hold and still access that
* component.
+ * <li> {@link android.content.Context#getSharedPreferences
+ * Context.getSharedPreferences()} will not automatically reload
+ * the preferences if they have changed on storage, unless
+ * {@link android.content.Context#MODE_MULTI_PROCESS} is used.
+ * <li> {@link android.view.ViewGroup#setMotionEventSplittingEnabled}
+ * will default to true.
+ * <li> {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH}
+ * is enabled by default on windows.
+ * <li> {@link android.widget.PopupWindow#isSplitTouchEnabled()
+ * PopupWindow.isSplitTouchEnabled()} will return true by default.
+ * <li> {@link android.widget.GridView} and {@link android.widget.ListView}
+ * will use {@link android.view.View#setActivated View.setActivated}
+ * for selected items if they do not implement {@link android.widget.Checkable}.
+ * <li> {@link android.widget.Scroller} will be constructed with
+ * "flywheel" behavior enabled by default.
* </ul>
*/
public static final int HONEYCOMB = 11;
@@ -266,13 +295,26 @@ public class Build {
* preferred over the older screen size buckets and for older devices
* the appropriate buckets will be inferred from them.</p>
*
- * <p>New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT}
+ * <p>Applications targeting this or a later release will get these
+ * new changes in behavior:</p>
+ * <ul>
+ * <li><p>New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT}
* and {@link android.content.pm.PackageManager#FEATURE_SCREEN_LANDSCAPE}
- * features are introduced in this release. Applications that target
+ * features were introduced in this release. Applications that target
* previous platform versions are assumed to require both portrait and
* landscape support in the device; when targeting Honeycomb MR1 or
* greater the application is responsible for specifying any specific
* orientation it requires.</p>
+ * <li><p>{@link android.os.AsyncTask} will use the serial executor
+ * by default when calling {@link android.os.AsyncTask#execute}.</p>
+ * <li><p>{@link android.content.pm.ActivityInfo#configChanges
+ * ActivityInfo.configChanges} will have the
+ * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE} and
+ * {@link android.content.pm.ActivityInfo#CONFIG_SMALLEST_SCREEN_SIZE}
+ * bits set; these need to be cleared for older applications because
+ * some developers have done absolute comparisons against this value
+ * instead of correctly masking the bits they are interested in.
+ * </ul>
*/
public static final int HONEYCOMB_MR2 = 13;
@@ -306,14 +348,31 @@ public class Build {
* <li> The fadingEdge attribute on views will be ignored (fading edges is no
* longer a standard part of the UI). A new requiresFadingEdge attribute allows
* applications to still force fading edges on for special cases.
+ * <li> {@link android.content.Context#bindService Context.bindService()}
+ * will not automatically add in {@link android.content.Context#BIND_WAIVE_PRIORITY}.
+ * <li> App Widgets will have standard padding automatically added around
+ * them, rather than relying on the padding being baked into the widget itself.
+ * <li> An exception will be thrown if you try to change the type of a
+ * window after it has been added to the window manager. Previously this
+ * would result in random incorrect behavior.
+ * <li> {@link android.view.animation.AnimationSet} will parse out
+ * the duration, fillBefore, fillAfter, repeatMode, and startOffset
+ * XML attributes that are defined.
+ * <li> {@link android.app.ActionBar#setHomeButtonEnabled
+ * ActionBar.setHomeButtonEnabled()} is false by default.
* </ul>
*/
public static final int ICE_CREAM_SANDWICH = 14;
/**
- * Android 4.0.3.
+ * December 2011: Android 4.0.3.
*/
public static final int ICE_CREAM_SANDWICH_MR1 = 15;
+
+ /**
+ * Next up on Android!
+ */
+ public static final int JELLY_BEAN = CUR_DEVELOPMENT;
}
/** The type of build, like "user" or "eng". */
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
index 9a53d76..270e9be 100644
--- a/core/java/android/os/IPowerManager.aidl
+++ b/core/java/android/os/IPowerManager.aidl
@@ -45,4 +45,5 @@ interface IPowerManager
// sets the brightness of the backlights (screen, keyboard, button) 0-255
void setBacklightBrightness(int brightness);
void setAttentionLight(boolean on, int color);
+ void setAutoBrightnessAdjustment(float adj);
}
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index e1bc275..cdf235d 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -219,6 +219,36 @@ public class Process {
public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1;
/**
+ * Default scheduling policy
+ * @hide
+ */
+ public static final int SCHED_OTHER = 0;
+
+ /**
+ * First-In First-Out scheduling policy
+ * @hide
+ */
+ public static final int SCHED_FIFO = 1;
+
+ /**
+ * Round-Robin scheduling policy
+ * @hide
+ */
+ public static final int SCHED_RR = 2;
+
+ /**
+ * Batch scheduling policy
+ * @hide
+ */
+ public static final int SCHED_BATCH = 3;
+
+ /**
+ * Idle scheduling policy
+ * @hide
+ */
+ public static final int SCHED_IDLE = 5;
+
+ /**
* Default thread group - gets a 'normal' share of the CPU
* @hide
*/
@@ -675,6 +705,24 @@ public class Process {
throws IllegalArgumentException;
/**
+ * Set the scheduling policy and priority of a thread, based on Linux.
+ *
+ * @param tid The identifier of the thread/process to change.
+ * @param policy A Linux scheduling policy such as SCHED_OTHER etc.
+ * @param priority A Linux priority level in a range appropriate for the given policy.
+ *
+ * @throws IllegalArgumentException Throws IllegalArgumentException if
+ * <var>tid</var> does not exist, or if <var>priority</var> is out of range for the policy.
+ * @throws SecurityException Throws SecurityException if your process does
+ * not have permission to modify the given thread, or to use the given
+ * scheduling policy or priority.
+ *
+ * {@hide}
+ */
+ public static final native void setThreadScheduler(int tid, int policy, int priority)
+ throws IllegalArgumentException;
+
+ /**
* Determine whether the current environment supports multiple processes.
*
* @return Returns true if the system can run in multiple processes, else
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index 73e8d98..43cf74e 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -26,6 +26,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
+import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
@@ -103,7 +104,12 @@ public class RecoverySystem {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
- trusted.add(cf.generateCertificate(zip.getInputStream(entry)));
+ InputStream is = zip.getInputStream(entry);
+ try {
+ trusted.add(cf.generateCertificate(is));
+ } finally {
+ is.close();
+ }
}
} finally {
zip.close();
@@ -162,8 +168,6 @@ public class RecoverySystem {
int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
- Log.v(TAG, String.format("comment size %d; signature start %d",
- commentSize, signatureStart));
byte[] eocd = new byte[commentSize + 22];
raf.seek(fileLen - (commentSize + 22));
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 83acef8..d724d56 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -6747,6 +6747,39 @@ public final class ContactsContract {
*/
public static final String NAMESPACE = DataColumns.DATA2;
}
+
+ /**
+ * <p>
+ * Convenient functionalities for "callable" data. Note that, this is NOT a separate data
+ * kind.
+ * </p>
+ * <p>
+ * This URI allows the ContactsProvider to return a unified result for "callable" data
+ * that users can use for calling purposes. {@link Phone} and {@link SipAddress} are the
+ * current examples for "callable", but may be expanded to the other types.
+ * </p>
+ * <p>
+ * Each returned row may have a different MIMETYPE and thus different interpretation for
+ * each column. For example the meaning for {@link Phone}'s type is different than
+ * {@link SipAddress}'s.
+ * </p>
+ *
+ * @hide
+ */
+ public static final class Callable implements DataColumnsWithJoins, CommonColumns {
+ /**
+ * Similar to {@link Phone#CONTENT_URI}, but returns callable data instead of only
+ * phone numbers.
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI,
+ "callables");
+ /**
+ * Similar to {@link Phone#CONTENT_FILTER_URI}, but allows users to filter callable
+ * data.
+ */
+ public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath(CONTENT_URI,
+ "filter");
+ }
}
/**
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 4e01672..d11219b 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -62,6 +62,14 @@ public final class MediaStore {
public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END";
/**
+ * The method name used by the media scanner and mtp to tell the media provider to
+ * rescan and reclassify that have become unhidden because of renaming folders or
+ * removing nomedia files
+ * @hide
+ */
+ public static final String UNHIDE_CALL = "unhide";
+
+ /**
* Activity Action: Launch a music player.
* The activity should be able to play, browse, or manipulate music files stored on the device.
*
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 84b0c8b..f14d27e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -569,7 +569,26 @@ public final class Settings {
"android.settings.DEVICE_INFO_SETTINGS";
/**
- * Activity Action: Show NFC sharing settings.
+ * Activity Action: Show NFC settings.
+ * <p>
+ * This shows UI that allows NFC to be turned on or off.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing
+ * @see android.nfc.NfcAdapter#isEnabled()
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_NFC_SETTINGS = "android.settings.NFC_SETTINGS";
+
+ /**
+ * Activity Action: Show NFC Sharing settings.
+ * <p>
+ * This shows UI that allows NDEF Push (Android Beam) to be turned on or
+ * off.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
* safeguard against this.
@@ -577,6 +596,7 @@ public final class Settings {
* Input: Nothing.
* <p>
* Output: Nothing
+ * @see android.nfc.NfcAdapter#isNdefPushEnabled()
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_NFCSHARING_SETTINGS =
@@ -727,7 +747,7 @@ public final class Settings {
Cursor c = null;
try {
c = cp.query(mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER,
- new String[]{name}, null);
+ new String[]{name}, null, null);
if (c == null) {
Log.w(TAG, "Can't get key " + name + " from " + mUri);
return null;
@@ -1381,6 +1401,12 @@ public final class Settings {
public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode";
/**
+ * Adjustment to auto-brightness to make it generally more (>0.0 <1.0)
+ * or less (<0.0 >-1.0) bright.
+ */
+ public static final String SCREEN_AUTO_BRIGHTNESS_ADJ = "screen_auto_brightness_adj";
+
+ /**
* SCREEN_BRIGHTNESS_MODE value for manual mode.
*/
public static final int SCREEN_BRIGHTNESS_MODE_MANUAL = 0;
@@ -1907,6 +1933,7 @@ public final class Settings {
SCREEN_OFF_TIMEOUT,
SCREEN_BRIGHTNESS,
SCREEN_BRIGHTNESS_MODE,
+ SCREEN_AUTO_BRIGHTNESS_ADJ,
VIBRATE_ON,
MODE_RINGER,
MODE_RINGER_STREAMS_AFFECTED,
@@ -2759,10 +2786,10 @@ public final class Settings {
public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password";
/**
- * If injection of accessibility enhancing JavaScript scripts
+ * If injection of accessibility enhancing JavaScript screen-reader
* is enabled.
* <p>
- * Note: Accessibility injecting scripts are served by the
+ * Note: The JavaScript based screen-reader is served by the
* Google infrastructure and enable users with disabilities to
* efficiantly navigate in and explore web content.
* </p>
@@ -2775,6 +2802,22 @@ public final class Settings {
"accessibility_script_injection";
/**
+ * The URL for the injected JavaScript based screen-reader used
+ * for providing accessiblity of content in WebView.
+ * <p>
+ * Note: The JavaScript based screen-reader is served by the
+ * Google infrastructure and enable users with disabilities to
+ * efficiently navigate in and explore web content.
+ * </p>
+ * <p>
+ * This property represents a string value.
+ * </p>
+ * @hide
+ */
+ public static final String ACCESSIBILITY_SCREEN_READER_URL =
+ "accessibility_script_injection_url";
+
+ /**
* Key bindings for navigation in built-in accessibility support for web content.
* <p>
* Note: These key bindings are for the built-in accessibility navigation for
@@ -4041,22 +4084,65 @@ public final class Settings {
public static final String SETUP_PREPAID_DETECTION_REDIR_HOST =
"setup_prepaid_detection_redir_host";
+ /**
+ * Whether the screensaver is enabled.
+ * @hide
+ */
+ public static final String SCREENSAVER_ENABLED = "screensaver_enabled";
+
+ /**
+ * The user's chosen screensaver component.
+ *
+ * This component will be launched by the PhoneWindowManager after a timeout when not on
+ * battery, or upon dock insertion (if SCREENSAVER_ACTIVATE_ON_DOCK is set to 1).
+ * @hide
+ */
+ public static final String SCREENSAVER_COMPONENT = "screensaver_component";
+
+ /**
+ * Whether the screensaver should be automatically launched when the device is inserted
+ * into a (desk) dock.
+ * @hide
+ */
+ public static final String SCREENSAVER_ACTIVATE_ON_DOCK = "screensaver_activate_on_dock";
+
/** {@hide} */
public static final String NETSTATS_ENABLED = "netstats_enabled";
/** {@hide} */
public static final String NETSTATS_POLL_INTERVAL = "netstats_poll_interval";
/** {@hide} */
- public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold";
+ public static final String NETSTATS_TIME_CACHE_MAX_AGE = "netstats_time_cache_max_age";
/** {@hide} */
- public static final String NETSTATS_NETWORK_BUCKET_DURATION = "netstats_network_bucket_duration";
+ public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes";
/** {@hide} */
- public static final String NETSTATS_NETWORK_MAX_HISTORY = "netstats_network_max_history";
+ public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled";
+
+ /** {@hide} */
+ public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration";
+ /** {@hide} */
+ public static final String NETSTATS_DEV_PERSIST_BYTES = "netstats_dev_persist_bytes";
+ /** {@hide} */
+ public static final String NETSTATS_DEV_ROTATE_AGE = "netstats_dev_rotate_age";
+ /** {@hide} */
+ public static final String NETSTATS_DEV_DELETE_AGE = "netstats_dev_delete_age";
+
/** {@hide} */
public static final String NETSTATS_UID_BUCKET_DURATION = "netstats_uid_bucket_duration";
/** {@hide} */
- public static final String NETSTATS_UID_MAX_HISTORY = "netstats_uid_max_history";
+ public static final String NETSTATS_UID_PERSIST_BYTES = "netstats_uid_persist_bytes";
+ /** {@hide} */
+ public static final String NETSTATS_UID_ROTATE_AGE = "netstats_uid_rotate_age";
+ /** {@hide} */
+ public static final String NETSTATS_UID_DELETE_AGE = "netstats_uid_delete_age";
+
+ /** {@hide} */
+ public static final String NETSTATS_UID_TAG_BUCKET_DURATION = "netstats_uid_tag_bucket_duration";
+ /** {@hide} */
+ public static final String NETSTATS_UID_TAG_PERSIST_BYTES = "netstats_uid_tag_persist_bytes";
+ /** {@hide} */
+ public static final String NETSTATS_UID_TAG_ROTATE_AGE = "netstats_uid_tag_rotate_age";
/** {@hide} */
- public static final String NETSTATS_TAG_MAX_HISTORY = "netstats_tag_max_history";
+ public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age";
/** Preferred NTP server. {@hide} */
public static final String NTP_SERVER = "ntp_server";
diff --git a/core/java/android/provider/UserDictionary.java b/core/java/android/provider/UserDictionary.java
index 5a7ef85..a9b106a 100644
--- a/core/java/android/provider/UserDictionary.java
+++ b/core/java/android/provider/UserDictionary.java
@@ -40,6 +40,9 @@ public class UserDictionary {
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY);
+ private static final int FREQUENCY_MIN = 0;
+ private static final int FREQUENCY_MAX = 255;
+
/**
* Contains the user defined words.
*/
@@ -87,12 +90,24 @@ public class UserDictionary {
*/
public static final String APP_ID = "appid";
- /** The locale type to specify that the word is common to all locales. */
+ /**
+ * An optional shortcut for this word. When the shortcut is typed, supporting IMEs should
+ * suggest the word in this row as an alternate spelling too.
+ */
+ public static final String SHORTCUT = "shortcut";
+
+ /**
+ * @deprecated Use {@link #addWord(Context, String, int, String, Locale)}.
+ */
+ @Deprecated
public static final int LOCALE_TYPE_ALL = 0;
-
- /** The locale type to specify that the word is for the current locale. */
+
+ /**
+ * @deprecated Use {@link #addWord(Context, String, int, String, Locale)}.
+ */
+ @Deprecated
public static final int LOCALE_TYPE_CURRENT = 1;
-
+
/**
* Sort by descending order of frequency.
*/
@@ -100,35 +115,65 @@ public class UserDictionary {
/** Adds a word to the dictionary, with the given frequency and the specified
* specified locale type.
+ *
+ * @deprecated Please use
+ * {@link #addWord(Context, String, int, String, Locale)} instead.
+ *
* @param context the current application context
* @param word the word to add to the dictionary. This should not be null or
* empty.
* @param localeType the locale type for this word. It should be one of
* {@link #LOCALE_TYPE_ALL} or {@link #LOCALE_TYPE_CURRENT}.
*/
- public static void addWord(Context context, String word,
+ @Deprecated
+ public static void addWord(Context context, String word,
int frequency, int localeType) {
- final ContentResolver resolver = context.getContentResolver();
- if (TextUtils.isEmpty(word) || localeType < 0 || localeType > 1) {
+ if (localeType != LOCALE_TYPE_ALL && localeType != LOCALE_TYPE_CURRENT) {
return;
}
-
- if (frequency < 0) frequency = 0;
- if (frequency > 255) frequency = 255;
- String locale = null;
+ final Locale locale;
- // TODO: Verify if this is the best way to get the current locale
if (localeType == LOCALE_TYPE_CURRENT) {
- locale = Locale.getDefault().toString();
+ locale = Locale.getDefault();
+ } else {
+ locale = null;
}
- ContentValues values = new ContentValues(4);
+
+ addWord(context, word, frequency, null, locale);
+ }
+
+ /** Adds a word to the dictionary, with the given frequency and the specified
+ * locale type.
+ *
+ * @param context the current application context
+ * @param word the word to add to the dictionary. This should not be null or
+ * empty.
+ * @param shortcut optional shortcut spelling for this word. When the shortcut
+ * is typed, the word may be suggested by applications that support it. May be null.
+ * @param locale the locale to insert the word for, or null to insert the word
+ * for all locales.
+ */
+ public static void addWord(Context context, String word,
+ int frequency, String shortcut, Locale locale) {
+ final ContentResolver resolver = context.getContentResolver();
+
+ if (TextUtils.isEmpty(word)) {
+ return;
+ }
+
+ if (frequency < FREQUENCY_MIN) frequency = FREQUENCY_MIN;
+ if (frequency > FREQUENCY_MAX) frequency = FREQUENCY_MAX;
+
+ final int COLUMN_COUNT = 5;
+ ContentValues values = new ContentValues(COLUMN_COUNT);
values.put(WORD, word);
values.put(FREQUENCY, frequency);
- values.put(LOCALE, locale);
+ values.put(LOCALE, null == locale ? null : locale.toString());
values.put(APP_ID, 0); // TODO: Get App UID
+ values.put(SHORTCUT, shortcut);
Uri result = resolver.insert(CONTENT_URI, values);
// It's ok if the insert doesn't succeed because the word
diff --git a/core/java/android/server/BluetoothAdapterStateMachine.java b/core/java/android/server/BluetoothAdapterStateMachine.java
index f617d95..7711caa 100644
--- a/core/java/android/server/BluetoothAdapterStateMachine.java
+++ b/core/java/android/server/BluetoothAdapterStateMachine.java
@@ -62,6 +62,17 @@ import java.io.PrintWriter;
* m1 = TURN_HOT
* m2 = Transition to HotOff when number of process wanting BT on is 0.
* POWER_STATE_CHANGED will make the transition.
+ * Note:
+ * The diagram above shows all the states and messages that trigger normal state changes.
+ * The diagram above does not capture everything:
+ * The diagram does not capture following messages.
+ * - messages that do not trigger state changes
+ * For example, PER_PROCESS_TURN_ON received in BluetoothOn state
+ * - unhandled messages
+ * For example, USER_TURN_ON received in BluetoothOn state
+ * - timeout messages
+ * The diagram does not capture error conditions and state recoveries.
+ * - For example POWER_STATE_CHANGED received in BluetoothOn state
*/
final class BluetoothAdapterStateMachine extends StateMachine {
private static final String TAG = "BluetoothAdapterStateMachine";
diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java
index bfad747..41bb87f 100644
--- a/core/java/android/server/BluetoothPanProfileHandler.java
+++ b/core/java/android/server/BluetoothPanProfileHandler.java
@@ -377,16 +377,16 @@ final class BluetoothPanProfileHandler {
try {
ifcg = service.getInterfaceConfig(iface);
if (ifcg != null) {
+ final LinkAddress linkAddr = ifcg.getLinkAddress();
InetAddress addr = null;
- if (ifcg.addr == null || (addr = ifcg.addr.getAddress()) == null ||
+ if (linkAddr == null || (addr = linkAddr.getAddress()) == null ||
addr.equals(NetworkUtils.numericToInetAddress("0.0.0.0")) ||
addr.equals(NetworkUtils.numericToInetAddress("::0"))) {
addr = NetworkUtils.numericToInetAddress(address);
}
- ifcg.interfaceFlags = ifcg.interfaceFlags.replace("down", "up");
- ifcg.addr = new LinkAddress(addr, BLUETOOTH_PREFIX_LENGTH);
- ifcg.interfaceFlags = ifcg.interfaceFlags.replace("running", "");
- ifcg.interfaceFlags = ifcg.interfaceFlags.replace(" "," ");
+ ifcg.setInterfaceUp();
+ ifcg.clearFlag("running");
+ ifcg.setLinkAddress(new LinkAddress(addr, BLUETOOTH_PREFIX_LENGTH));
service.setInterfaceConfig(iface, ifcg);
if (cm.tether(iface) != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
Log.e(TAG, "Error tethering "+iface);
diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java
index 83ea874..2b8a458 100644
--- a/core/java/android/service/textservice/SpellCheckerService.java
+++ b/core/java/android/service/textservice/SpellCheckerService.java
@@ -139,6 +139,25 @@ public abstract class SpellCheckerService extends Service {
}
/**
+ * @hide
+ * The default implementation returns an array of SuggestionsInfo by simply calling
+ * onGetSuggestions().
+ * When you override this method, make sure that suggestionsLimit is applied to suggestions
+ * that share the same start position and length.
+ */
+ public SuggestionsInfo[] onGetSuggestionsMultipleForSentence(TextInfo[] textInfos,
+ int suggestionsLimit) {
+ final int length = textInfos.length;
+ final SuggestionsInfo[] retval = new SuggestionsInfo[length];
+ for (int i = 0; i < length; ++i) {
+ retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit);
+ retval[i].setCookieAndSequence(
+ textInfos[i].getCookie(), textInfos[i].getSequence());
+ }
+ return retval;
+ }
+
+ /**
* Request to abort all tasks executed in SpellChecker.
* This function will run on the incoming IPC thread.
* So, this is not called on the main thread,
@@ -201,6 +220,16 @@ public abstract class SpellCheckerService extends Service {
}
@Override
+ public void onGetSuggestionsMultipleForSentence(
+ TextInfo[] textInfos, int suggestionsLimit) {
+ try {
+ mListener.onGetSuggestionsForSentence(
+ mSession.onGetSuggestionsMultipleForSentence(textInfos, suggestionsLimit));
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
public void onCancel() {
int pri = Process.getThreadPriority(Process.myTid());
try {
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 18167b6..7ce96c0 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -18,7 +18,6 @@ package android.service.wallpaper;
import com.android.internal.os.HandlerCaller;
import com.android.internal.view.BaseIWindow;
-import com.android.internal.view.BaseInputHandler;
import com.android.internal.view.BaseSurfaceHolder;
import android.annotation.SdkConstant;
@@ -45,8 +44,8 @@ import android.view.Gravity;
import android.view.IWindowSession;
import android.view.InputChannel;
import android.view.InputDevice;
-import android.view.InputHandler;
-import android.view.InputQueue;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.View;
@@ -228,24 +227,29 @@ public abstract class WallpaperService extends Service {
}
};
-
- final InputHandler mInputHandler = new BaseInputHandler() {
+
+ final class WallpaperInputEventReceiver extends InputEventReceiver {
+ public WallpaperInputEventReceiver(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
+
@Override
- public void handleMotion(MotionEvent event,
- InputQueue.FinishedCallback finishedCallback) {
+ public void onInputEvent(InputEvent event) {
boolean handled = false;
try {
- int source = event.getSource();
- if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
- dispatchPointer(event);
+ if (event instanceof MotionEvent
+ && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ MotionEvent dup = MotionEvent.obtainNoHistory((MotionEvent)event);
+ dispatchPointer(dup);
handled = true;
}
} finally {
- finishedCallback.finished(handled);
+ finishInputEvent(event, handled);
}
}
- };
-
+ }
+ WallpaperInputEventReceiver mInputEventReceiver;
+
final BaseIWindow mWindow = new BaseIWindow() {
@Override
public void resized(int w, int h, Rect coveredInsets,
@@ -534,6 +538,8 @@ public abstract class WallpaperService extends Service {
}
Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);
mCaller.sendMessage(msg);
+ } else {
+ event.recycle();
}
}
@@ -599,8 +605,8 @@ public abstract class WallpaperService extends Service {
}
mCreated = true;
- InputQueue.registerInputChannel(mInputChannel, mInputHandler,
- Looper.myQueue());
+ mInputEventReceiver = new WallpaperInputEventReceiver(
+ mInputChannel, Looper.myLooper());
}
mSurfaceHolder.mSurfaceLock.lock();
@@ -902,8 +908,9 @@ public abstract class WallpaperService extends Service {
if (DEBUG) Log.v(TAG, "Removing window and destroying surface "
+ mSurfaceHolder.getSurface() + " of: " + this);
- if (mInputChannel != null) {
- InputQueue.unregisterInputChannel(mInputChannel);
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
}
mSession.remove(mWindow);
@@ -970,6 +977,8 @@ public abstract class WallpaperService extends Service {
public void dispatchPointer(MotionEvent event) {
if (mEngine != null) {
mEngine.dispatchPointer(event);
+ } else {
+ event.recycle();
}
}
diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioMessageParams.java
deleted file mode 100644
index 29b4367..0000000
--- a/core/java/android/speech/tts/AudioMessageParams.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.speech.tts;
-
-import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
-
-class AudioMessageParams extends MessageParams {
- private final BlockingMediaPlayer mPlayer;
-
- AudioMessageParams(UtteranceProgressDispatcher dispatcher,
- String callingApp, BlockingMediaPlayer player) {
- super(dispatcher, callingApp);
- mPlayer = player;
- }
-
- BlockingMediaPlayer getPlayer() {
- return mPlayer;
- }
-
- @Override
- int getType() {
- return TYPE_AUDIO;
- }
-
-}
diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java
index 46a78dc..d63f605 100644
--- a/core/java/android/speech/tts/AudioPlaybackHandler.java
+++ b/core/java/android/speech/tts/AudioPlaybackHandler.java
@@ -15,44 +15,20 @@
*/
package android.speech.tts;
-import android.media.AudioFormat;
-import android.media.AudioTrack;
-import android.text.TextUtils;
import android.util.Log;
import java.util.Iterator;
-import java.util.concurrent.PriorityBlockingQueue;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.LinkedBlockingQueue;
class AudioPlaybackHandler {
private static final String TAG = "TTS.AudioPlaybackHandler";
- private static final boolean DBG_THREADING = false;
private static final boolean DBG = false;
- private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
-
- private static final int SYNTHESIS_START = 1;
- private static final int SYNTHESIS_DATA_AVAILABLE = 2;
- private static final int SYNTHESIS_DONE = 3;
-
- private static final int PLAY_AUDIO = 5;
- private static final int PLAY_SILENCE = 6;
-
- private static final int SHUTDOWN = -1;
-
- private static final int DEFAULT_PRIORITY = 1;
- private static final int HIGH_PRIORITY = 0;
-
- private final PriorityBlockingQueue<ListEntry> mQueue =
- new PriorityBlockingQueue<ListEntry>();
+ private final LinkedBlockingQueue<PlaybackQueueItem> mQueue =
+ new LinkedBlockingQueue<PlaybackQueueItem>();
private final Thread mHandlerThread;
- private volatile MessageParams mCurrentParams = null;
- // Used only for book keeping and error detection.
- private volatile SynthesisMessageParams mLastSynthesisRequest = null;
- // Used to order incoming messages in our priority queue.
- private final AtomicLong mSequenceIdCtr = new AtomicLong(0);
-
+ private volatile PlaybackQueueItem mCurrentWorkItem = null;
AudioPlaybackHandler() {
mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread");
@@ -62,82 +38,38 @@ class AudioPlaybackHandler {
mHandlerThread.start();
}
- /**
- * Stops all synthesis for a given {@code token}. If the current token
- * is currently being processed, an effort will be made to stop it but
- * that is not guaranteed.
- *
- * NOTE: This assumes that all other messages in the queue with {@code token}
- * have been removed already.
- *
- * NOTE: Must be called synchronized on {@code AudioPlaybackHandler.this}.
- */
- private void stop(MessageParams token) {
- if (token == null) {
+ private void stop(PlaybackQueueItem item) {
+ if (item == null) {
return;
}
- if (DBG) Log.d(TAG, "Stopping token : " + token);
+ item.stop(false);
+ }
- if (token.getType() == MessageParams.TYPE_SYNTHESIS) {
- AudioTrack current = ((SynthesisMessageParams) token).getAudioTrack();
- if (current != null) {
- // Stop the current audio track if it's still playing.
- // The audio track is thread safe in this regard. The current
- // handleSynthesisDataAvailable call will return soon after this
- // call.
- current.stop();
- }
- // This is safe because PlaybackSynthesisCallback#stop would have
- // been called before this method, and will no longer enqueue any
- // audio for this token.
- //
- // (Even if it did, all it would result in is a warning message).
- mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY));
- } else if (token.getType() == MessageParams.TYPE_AUDIO) {
- ((AudioMessageParams) token).getPlayer().stop();
- // No cleanup required for audio messages.
- } else if (token.getType() == MessageParams.TYPE_SILENCE) {
- ((SilenceMessageParams) token).getConditionVariable().open();
- // No cleanup required for silence messages.
+ public void enqueue(PlaybackQueueItem item) {
+ try {
+ mQueue.put(item);
+ } catch (InterruptedException ie) {
+ // This exception will never be thrown, since we allow our queue
+ // to be have an unbounded size. put() will therefore never block.
}
}
- // -----------------------------------------------------
- // Methods that add and remove elements from the queue. These do not
- // need to be synchronized strictly speaking, but they make the behaviour
- // a lot more predictable. (though it would still be correct without
- // synchronization).
- // -----------------------------------------------------
-
- synchronized public void removePlaybackItems(String callingApp) {
- if (DBG_THREADING) Log.d(TAG, "Removing all callback items for : " + callingApp);
- removeMessages(callingApp);
+ public void stopForApp(Object callerIdentity) {
+ if (DBG) Log.d(TAG, "Removing all callback items for : " + callerIdentity);
+ removeWorkItemsFor(callerIdentity);
- final MessageParams current = getCurrentParams();
- if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) {
+ final PlaybackQueueItem current = mCurrentWorkItem;
+ if (current != null && (current.getCallerIdentity() == callerIdentity)) {
stop(current);
}
-
- final MessageParams lastSynthesis = mLastSynthesisRequest;
-
- if (lastSynthesis != null && lastSynthesis != current &&
- TextUtils.equals(callingApp, lastSynthesis.getCallingApp())) {
- stop(lastSynthesis);
- }
}
- synchronized public void removeAllItems() {
- if (DBG_THREADING) Log.d(TAG, "Removing all items");
+ public void stop() {
+ if (DBG) Log.d(TAG, "Stopping all items");
removeAllMessages();
- final MessageParams current = getCurrentParams();
- final MessageParams lastSynthesis = mLastSynthesisRequest;
- stop(current);
-
- if (lastSynthesis != null && lastSynthesis != current) {
- stop(lastSynthesis);
- }
+ stop(mCurrentWorkItem);
}
/**
@@ -145,489 +77,64 @@ class AudioPlaybackHandler {
* being handled, true otherwise.
*/
public boolean isSpeaking() {
- return (mQueue.peek() != null) || (mCurrentParams != null);
+ return (mQueue.peek() != null) || (mCurrentWorkItem != null);
}
/**
* Shut down the audio playback thread.
*/
- synchronized public void quit() {
+ public void quit() {
removeAllMessages();
- stop(getCurrentParams());
- mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY));
- }
-
- synchronized void enqueueSynthesisStart(SynthesisMessageParams token) {
- if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis start : " + token);
- mQueue.add(new ListEntry(SYNTHESIS_START, token));
- }
-
- synchronized void enqueueSynthesisDataAvailable(SynthesisMessageParams token) {
- if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis data available : " + token);
- mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token));
- }
-
- synchronized void enqueueSynthesisDone(SynthesisMessageParams token) {
- if (DBG_THREADING) Log.d(TAG, "Enqueuing synthesis done : " + token);
- mQueue.add(new ListEntry(SYNTHESIS_DONE, token));
- }
-
- synchronized void enqueueAudio(AudioMessageParams token) {
- if (DBG_THREADING) Log.d(TAG, "Enqueuing audio : " + token);
- mQueue.add(new ListEntry(PLAY_AUDIO, token));
- }
-
- synchronized void enqueueSilence(SilenceMessageParams token) {
- if (DBG_THREADING) Log.d(TAG, "Enqueuing silence : " + token);
- mQueue.add(new ListEntry(PLAY_SILENCE, token));
- }
-
- // -----------------------------------------
- // End of public API methods.
- // -----------------------------------------
-
- // -----------------------------------------
- // Methods for managing the message queue.
- // -----------------------------------------
-
- /*
- * The MessageLoop is a handler like implementation that
- * processes messages from a priority queue.
- */
- private final class MessageLoop implements Runnable {
- @Override
- public void run() {
- while (true) {
- ListEntry entry = null;
- try {
- entry = mQueue.take();
- } catch (InterruptedException ie) {
- return;
- }
-
- if (entry.mWhat == SHUTDOWN) {
- if (DBG) Log.d(TAG, "MessageLoop : Shutting down");
- return;
- }
-
- if (DBG) {
- Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat
- + " ,seqId : " + entry.mSequenceId);
- }
-
- setCurrentParams(entry.mMessage);
- handleMessage(entry);
- setCurrentParams(null);
- }
- }
+ stop(mCurrentWorkItem);
+ mHandlerThread.interrupt();
}
/*
* Atomically clear the queue of all messages.
*/
- synchronized private void removeAllMessages() {
+ private void removeAllMessages() {
mQueue.clear();
}
/*
* Remove all messages that originate from a given calling app.
*/
- synchronized private void removeMessages(String callingApp) {
- Iterator<ListEntry> it = mQueue.iterator();
+ private void removeWorkItemsFor(Object callerIdentity) {
+ Iterator<PlaybackQueueItem> it = mQueue.iterator();
while (it.hasNext()) {
- final ListEntry current = it.next();
- // The null check is to prevent us from removing control messages,
- // such as a shutdown message.
- if (current.mMessage != null &&
- callingApp.equals(current.mMessage.getCallingApp())) {
+ final PlaybackQueueItem item = it.next();
+ if (item.getCallerIdentity() == callerIdentity) {
it.remove();
}
}
}
/*
- * An element of our priority queue of messages. Each message has a priority,
- * and a sequence id (defined by the order of enqueue calls). Among messages
- * with the same priority, messages that were received earlier win out.
+ * The MessageLoop is a handler like implementation that
+ * processes messages from a priority queue.
*/
- private final class ListEntry implements Comparable<ListEntry> {
- final int mWhat;
- final MessageParams mMessage;
- final int mPriority;
- final long mSequenceId;
-
- private ListEntry(int what, MessageParams message) {
- this(what, message, DEFAULT_PRIORITY);
- }
-
- private ListEntry(int what, MessageParams message, int priority) {
- mWhat = what;
- mMessage = message;
- mPriority = priority;
- mSequenceId = mSequenceIdCtr.incrementAndGet();
- }
-
+ private final class MessageLoop implements Runnable {
@Override
- public int compareTo(ListEntry that) {
- if (that == this) {
- return 0;
- }
-
- // Note that this is always 0, 1 or -1.
- int priorityDiff = mPriority - that.mPriority;
- if (priorityDiff == 0) {
- // The == case cannot occur.
- return (mSequenceId < that.mSequenceId) ? -1 : 1;
- }
-
- return priorityDiff;
- }
- }
-
- private void setCurrentParams(MessageParams p) {
- if (DBG_THREADING) {
- if (p != null) {
- Log.d(TAG, "Started handling :" + p);
- } else {
- Log.d(TAG, "End handling : " + mCurrentParams);
- }
- }
- mCurrentParams = p;
- }
-
- private MessageParams getCurrentParams() {
- return mCurrentParams;
- }
-
- // -----------------------------------------
- // Methods for dealing with individual messages, the methods
- // below do the actual work.
- // -----------------------------------------
-
- private void handleMessage(ListEntry entry) {
- final MessageParams msg = entry.mMessage;
- if (entry.mWhat == SYNTHESIS_START) {
- handleSynthesisStart(msg);
- } else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) {
- handleSynthesisDataAvailable(msg);
- } else if (entry.mWhat == SYNTHESIS_DONE) {
- handleSynthesisDone(msg);
- } else if (entry.mWhat == PLAY_AUDIO) {
- handleAudio(msg);
- } else if (entry.mWhat == PLAY_SILENCE) {
- handleSilence(msg);
- }
- }
-
- // Currently implemented as blocking the audio playback thread for the
- // specified duration. If a call to stop() is made, the thread
- // unblocks.
- private void handleSilence(MessageParams msg) {
- if (DBG) Log.d(TAG, "handleSilence()");
- SilenceMessageParams params = (SilenceMessageParams) msg;
- params.getDispatcher().dispatchOnStart();
- if (params.getSilenceDurationMs() > 0) {
- params.getConditionVariable().block(params.getSilenceDurationMs());
- }
- params.getDispatcher().dispatchOnDone();
- if (DBG) Log.d(TAG, "handleSilence() done.");
- }
-
- // Plays back audio from a given URI. No TTS engine involvement here.
- private void handleAudio(MessageParams msg) {
- if (DBG) Log.d(TAG, "handleAudio()");
- AudioMessageParams params = (AudioMessageParams) msg;
- params.getDispatcher().dispatchOnStart();
- // Note that the BlockingMediaPlayer spawns a separate thread.
- //
- // TODO: This can be avoided.
- params.getPlayer().startAndWait();
- params.getDispatcher().dispatchOnDone();
- if (DBG) Log.d(TAG, "handleAudio() done.");
- }
-
- // Denotes the start of a new synthesis request. We create a new
- // audio track, and prepare it for incoming data.
- //
- // Note that since all TTS synthesis happens on a single thread, we
- // should ALWAYS see the following order :
- //
- // handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone
- // OR
- // handleSynthesisCompleteDataAvailable.
- private void handleSynthesisStart(MessageParams msg) {
- if (DBG) Log.d(TAG, "handleSynthesisStart()");
- final SynthesisMessageParams param = (SynthesisMessageParams) msg;
-
- // Oops, looks like the engine forgot to call done(). We go through
- // extra trouble to clean the data to prevent the AudioTrack resources
- // from being leaked.
- if (mLastSynthesisRequest != null) {
- Log.e(TAG, "Error : Missing call to done() for request : " +
- mLastSynthesisRequest);
- handleSynthesisDone(mLastSynthesisRequest);
- }
-
- mLastSynthesisRequest = param;
-
- // Create the audio track.
- final AudioTrack audioTrack = createStreamingAudioTrack(param);
-
- if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]");
-
- param.setAudioTrack(audioTrack);
- msg.getDispatcher().dispatchOnStart();
- }
-
- // More data available to be flushed to the audio track.
- private void handleSynthesisDataAvailable(MessageParams msg) {
- final SynthesisMessageParams param = (SynthesisMessageParams) msg;
- if (param.getAudioTrack() == null) {
- Log.w(TAG, "Error : null audio track in handleDataAvailable : " + param);
- return;
- }
-
- if (param != mLastSynthesisRequest) {
- Log.e(TAG, "Call to dataAvailable without done() / start()");
- return;
- }
-
- final AudioTrack audioTrack = param.getAudioTrack();
- final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer();
-
- if (bufferCopy == null) {
- Log.e(TAG, "No buffers available to play.");
- return;
- }
-
- int playState = audioTrack.getPlayState();
- if (playState == AudioTrack.PLAYSTATE_STOPPED) {
- if (DBG) Log.d(TAG, "AudioTrack stopped, restarting : " + audioTrack.hashCode());
- audioTrack.play();
- }
- int count = 0;
- while (count < bufferCopy.mBytes.length) {
- // Note that we don't take bufferCopy.mOffset into account because
- // it is guaranteed to be 0.
- int written = audioTrack.write(bufferCopy.mBytes, count, bufferCopy.mBytes.length);
- if (written <= 0) {
- break;
- }
- count += written;
- }
- param.mBytesWritten += count;
- param.mLogger.onPlaybackStart();
- }
-
- // Wait for the audio track to stop playing, and then release its resources.
- private void handleSynthesisDone(MessageParams msg) {
- final SynthesisMessageParams params = (SynthesisMessageParams) msg;
-
- if (DBG) Log.d(TAG, "handleSynthesisDone()");
- final AudioTrack audioTrack = params.getAudioTrack();
-
- if (audioTrack == null) {
- // There was already a call to handleSynthesisDone for
- // this token.
- return;
- }
-
- if (params.mBytesWritten < params.mAudioBufferSize) {
- if (DBG) Log.d(TAG, "Stopping audio track to flush audio, state was : " +
- audioTrack.getPlayState());
- params.mIsShortUtterance = true;
- audioTrack.stop();
- }
-
- if (DBG) Log.d(TAG, "Waiting for audio track to complete : " +
- audioTrack.hashCode());
- blockUntilDone(params);
- if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]");
-
- // The last call to AudioTrack.write( ) will return only after
- // all data from the audioTrack has been sent to the mixer, so
- // it's safe to release at this point. Make sure release() and the call
- // that set the audio track to null are performed atomically.
- synchronized (this) {
- // Never allow the audioTrack to be observed in a state where
- // it is released but non null. The only case this might happen
- // is in the various stopFoo methods that call AudioTrack#stop from
- // different threads, but they are synchronized on AudioPlayBackHandler#this
- // too.
- audioTrack.release();
- params.setAudioTrack(null);
- }
- if (params.isError()) {
- params.getDispatcher().dispatchOnError();
- } else {
- params.getDispatcher().dispatchOnDone();
- }
- mLastSynthesisRequest = null;
- params.mLogger.onWriteData();
- }
-
- /**
- * The minimum increment of time to wait for an audiotrack to finish
- * playing.
- */
- private static final long MIN_SLEEP_TIME_MS = 20;
-
- /**
- * The maximum increment of time to sleep while waiting for an audiotrack
- * to finish playing.
- */
- private static final long MAX_SLEEP_TIME_MS = 2500;
-
- /**
- * The maximum amount of time to wait for an audio track to make progress while
- * it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but
- * could happen in exceptional circumstances like a media_server crash.
- */
- private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS;
-
- private static void blockUntilDone(SynthesisMessageParams params) {
- if (params.mAudioTrack == null || params.mBytesWritten <= 0) {
- return;
- }
-
- if (params.mIsShortUtterance) {
- // In this case we would have called AudioTrack#stop() to flush
- // buffers to the mixer. This makes the playback head position
- // unobservable and notification markers do not work reliably. We
- // have no option but to wait until we think the track would finish
- // playing and release it after.
- //
- // This isn't as bad as it looks because (a) We won't end up waiting
- // for much longer than we should because even at 4khz mono, a short
- // utterance weighs in at about 2 seconds, and (b) such short utterances
- // are expected to be relatively infrequent and in a stream of utterances
- // this shows up as a slightly longer pause.
- blockUntilEstimatedCompletion(params);
- } else {
- blockUntilCompletion(params);
- }
- }
-
- private static void blockUntilEstimatedCompletion(SynthesisMessageParams params) {
- final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame;
- final long estimatedTimeMs = (lengthInFrames * 1000 / params.mSampleRateInHz);
-
- if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance");
-
- try {
- Thread.sleep(estimatedTimeMs);
- } catch (InterruptedException ie) {
- // Do nothing.
- }
- }
-
- private static void blockUntilCompletion(SynthesisMessageParams params) {
- final AudioTrack audioTrack = params.mAudioTrack;
- final int lengthInFrames = params.mBytesWritten / params.mBytesPerFrame;
-
- int previousPosition = -1;
- int currentPosition = 0;
- long blockedTimeMs = 0;
-
- while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames &&
- audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
-
- final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
- audioTrack.getSampleRate();
- final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS);
-
- // Check if the audio track has made progress since the last loop
- // iteration. We should then add in the amount of time that was
- // spent sleeping in the last iteration.
- if (currentPosition == previousPosition) {
- // This works only because the sleep time that would have been calculated
- // would be the same in the previous iteration too.
- blockedTimeMs += sleepTimeMs;
- // If we've taken too long to make progress, bail.
- if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) {
- Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " +
- "for AudioTrack to make progress, Aborting");
- break;
+ public void run() {
+ while (true) {
+ PlaybackQueueItem item = null;
+ try {
+ item = mQueue.take();
+ } catch (InterruptedException ie) {
+ if (DBG) Log.d(TAG, "MessageLoop : Shutting down (interrupted)");
+ return;
}
- } else {
- blockedTimeMs = 0;
- }
- previousPosition = currentPosition;
- if (DBG) Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," +
- " Playback position : " + currentPosition + ", Length in frames : "
- + lengthInFrames);
- try {
- Thread.sleep(sleepTimeMs);
- } catch (InterruptedException ie) {
- break;
- }
- }
- }
-
- private static final long clip(long value, long min, long max) {
- if (value < min) {
- return min;
- }
-
- if (value > max) {
- return max;
- }
-
- return value;
- }
-
- private static AudioTrack createStreamingAudioTrack(SynthesisMessageParams params) {
- final int channelConfig = getChannelConfig(params.mChannelCount);
- final int sampleRateInHz = params.mSampleRateInHz;
- final int audioFormat = params.mAudioFormat;
-
- int minBufferSizeInBytes
- = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
- int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
-
- AudioTrack audioTrack = new AudioTrack(params.mStreamType, sampleRateInHz, channelConfig,
- audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
- if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
- Log.w(TAG, "Unable to create audio track.");
- audioTrack.release();
- return null;
- }
- params.mAudioBufferSize = bufferSizeInBytes;
+ // If stop() or stopForApp() are called between mQueue.take()
+ // returning and mCurrentWorkItem being set, the current work item
+ // will be run anyway.
- setupVolume(audioTrack, params.mVolume, params.mPan);
- return audioTrack;
- }
-
- static int getChannelConfig(int channelCount) {
- if (channelCount == 1) {
- return AudioFormat.CHANNEL_OUT_MONO;
- } else if (channelCount == 2){
- return AudioFormat.CHANNEL_OUT_STEREO;
- }
-
- return 0;
- }
-
- private static void setupVolume(AudioTrack audioTrack, float volume, float pan) {
- float vol = clip(volume, 0.0f, 1.0f);
- float panning = clip(pan, -1.0f, 1.0f);
- float volLeft = vol;
- float volRight = vol;
- if (panning > 0.0f) {
- volLeft *= (1.0f - panning);
- } else if (panning < 0.0f) {
- volRight *= (1.0f + panning);
- }
- if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
- if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
- Log.e(TAG, "Failed to set volume");
+ mCurrentWorkItem = item;
+ item.run();
+ mCurrentWorkItem = null;
+ }
}
}
- private static float clip(float value, float min, float max) {
- return value > max ? max : (value < min ? min : value);
- }
-
}
diff --git a/core/java/android/speech/tts/BlockingMediaPlayer.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java
index 3cf60dd..1a1fda8 100644
--- a/core/java/android/speech/tts/BlockingMediaPlayer.java
+++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java
@@ -19,93 +19,44 @@ import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.ConditionVariable;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
+import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
import android.util.Log;
-/**
- * A media player that allows blocking to wait for it to finish.
- */
-class BlockingMediaPlayer {
-
- private static final String TAG = "BlockMediaPlayer";
-
- private static final String MEDIA_PLAYER_THREAD_NAME = "TTS-MediaPlayer";
+class AudioPlaybackQueueItem extends PlaybackQueueItem {
+ private static final String TAG = "TTS.AudioQueueItem";
private final Context mContext;
private final Uri mUri;
private final int mStreamType;
+
private final ConditionVariable mDone;
- // Only accessed on the Handler thread
private MediaPlayer mPlayer;
private volatile boolean mFinished;
- /**
- * Creates a new blocking media player.
- * Creating a blocking media player is a cheap operation.
- *
- * @param context
- * @param uri
- * @param streamType
- */
- public BlockingMediaPlayer(Context context, Uri uri, int streamType) {
+ AudioPlaybackQueueItem(UtteranceProgressDispatcher dispatcher,
+ Object callerIdentity,
+ Context context, Uri uri, int streamType) {
+ super(dispatcher, callerIdentity);
+
mContext = context;
mUri = uri;
mStreamType = streamType;
- mDone = new ConditionVariable();
-
- }
- /**
- * Starts playback and waits for it to finish.
- * Can be called from any thread.
- *
- * @return {@code true} if the playback finished normally, {@code false} if the playback
- * failed or {@link #stop} was called before the playback finished.
- */
- public boolean startAndWait() {
- HandlerThread thread = new HandlerThread(MEDIA_PLAYER_THREAD_NAME);
- thread.start();
- Handler handler = new Handler(thread.getLooper());
+ mDone = new ConditionVariable();
+ mPlayer = null;
mFinished = false;
- handler.post(new Runnable() {
- @Override
- public void run() {
- startPlaying();
- }
- });
- mDone.block();
- handler.post(new Runnable() {
- @Override
- public void run() {
- finish();
- // No new messages should get posted to the handler thread after this
- Looper.myLooper().quit();
- }
- });
- return mFinished;
}
+ @Override
+ public void run() {
+ final UtteranceProgressDispatcher dispatcher = getDispatcher();
- /**
- * Stops playback. Can be called multiple times.
- * Can be called from any thread.
- */
- public void stop() {
- mDone.open();
- }
-
- /**
- * Starts playback.
- * Called on the handler thread.
- */
- private void startPlaying() {
+ dispatcher.dispatchOnStart();
mPlayer = MediaPlayer.create(mContext, mUri);
if (mPlayer == null) {
- Log.w(TAG, "Failed to play " + mUri);
- mDone.open();
+ dispatcher.dispatchOnError();
return;
}
+
try {
mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
@@ -124,16 +75,20 @@ class BlockingMediaPlayer {
});
mPlayer.setAudioStreamType(mStreamType);
mPlayer.start();
+ mDone.block();
+ finish();
} catch (IllegalArgumentException ex) {
Log.w(TAG, "MediaPlayer failed", ex);
mDone.open();
}
+
+ if (mFinished) {
+ dispatcher.dispatchOnDone();
+ } else {
+ dispatcher.dispatchOnError();
+ }
}
- /**
- * Stops playback and release the media player.
- * Called on the handler thread.
- */
private void finish() {
try {
mPlayer.stop();
@@ -143,4 +98,8 @@ class BlockingMediaPlayer {
mPlayer.release();
}
-} \ No newline at end of file
+ @Override
+ void stop(boolean isError) {
+ mDone.open();
+ }
+}
diff --git a/core/java/android/speech/tts/BlockingAudioTrack.java b/core/java/android/speech/tts/BlockingAudioTrack.java
new file mode 100644
index 0000000..fcadad7
--- /dev/null
+++ b/core/java/android/speech/tts/BlockingAudioTrack.java
@@ -0,0 +1,338 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package android.speech.tts;
+
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * Exposes parts of the {@link AudioTrack} API by delegating calls to an
+ * underlying {@link AudioTrack}. Additionally, provides methods like
+ * {@link #waitAndRelease()} that will block until all audiotrack
+ * data has been flushed to the mixer, and is estimated to have completed
+ * playback.
+ */
+class BlockingAudioTrack {
+ private static final String TAG = "TTS.BlockingAudioTrack";
+ private static final boolean DBG = false;
+
+
+ /**
+ * The minimum increment of time to wait for an AudioTrack to finish
+ * playing.
+ */
+ private static final long MIN_SLEEP_TIME_MS = 20;
+
+ /**
+ * The maximum increment of time to sleep while waiting for an AudioTrack
+ * to finish playing.
+ */
+ private static final long MAX_SLEEP_TIME_MS = 2500;
+
+ /**
+ * The maximum amount of time to wait for an audio track to make progress while
+ * it remains in PLAYSTATE_PLAYING. This should never happen in normal usage, but
+ * could happen in exceptional circumstances like a media_server crash.
+ */
+ private static final long MAX_PROGRESS_WAIT_MS = MAX_SLEEP_TIME_MS;
+
+ /**
+ * Minimum size of the buffer of the underlying {@link android.media.AudioTrack}
+ * we create.
+ */
+ private static final int MIN_AUDIO_BUFFER_SIZE = 8192;
+
+
+ private final int mStreamType;
+ private final int mSampleRateInHz;
+ private final int mAudioFormat;
+ private final int mChannelCount;
+ private final float mVolume;
+ private final float mPan;
+
+ private final int mBytesPerFrame;
+ /**
+ * A "short utterance" is one that uses less bytes than the audio
+ * track buffer size (mAudioBufferSize). In this case, we need to call
+ * {@link AudioTrack#stop()} to send pending buffers to the mixer, and slightly
+ * different logic is required to wait for the track to finish.
+ *
+ * Not volatile, accessed only from the audio playback thread.
+ */
+ private boolean mIsShortUtterance;
+ /**
+ * Will be valid after a call to {@link #init()}.
+ */
+ private int mAudioBufferSize;
+ private int mBytesWritten = 0;
+
+ private AudioTrack mAudioTrack;
+ private volatile boolean mStopped;
+ // Locks the initialization / uninitialization of the audio track.
+ // This is required because stop() will throw an illegal state exception
+ // if called before init() or after mAudioTrack.release().
+ private final Object mAudioTrackLock = new Object();
+
+ BlockingAudioTrack(int streamType, int sampleRate,
+ int audioFormat, int channelCount,
+ float volume, float pan) {
+ mStreamType = streamType;
+ mSampleRateInHz = sampleRate;
+ mAudioFormat = audioFormat;
+ mChannelCount = channelCount;
+ mVolume = volume;
+ mPan = pan;
+
+ mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount;
+ mIsShortUtterance = false;
+ mAudioBufferSize = 0;
+ mBytesWritten = 0;
+
+ mAudioTrack = null;
+ mStopped = false;
+ }
+
+ public void init() {
+ AudioTrack track = createStreamingAudioTrack();
+
+ synchronized (mAudioTrackLock) {
+ mAudioTrack = track;
+ }
+ }
+
+ public void stop() {
+ synchronized (mAudioTrackLock) {
+ if (mAudioTrack != null) {
+ mAudioTrack.stop();
+ }
+ }
+ mStopped = true;
+ }
+
+ public int write(byte[] data) {
+ if (mAudioTrack == null || mStopped) {
+ return -1;
+ }
+ final int bytesWritten = writeToAudioTrack(mAudioTrack, data);
+ mBytesWritten += bytesWritten;
+ return bytesWritten;
+ }
+
+ public void waitAndRelease() {
+ // For "small" audio tracks, we have to stop() them to make them mixable,
+ // else the audio subsystem will wait indefinitely for us to fill the buffer
+ // before rendering the track mixable.
+ //
+ // If mStopped is true, the track would already have been stopped, so not
+ // much point not doing that again.
+ if (mBytesWritten < mAudioBufferSize && !mStopped) {
+ if (DBG) {
+ Log.d(TAG, "Stopping audio track to flush audio, state was : " +
+ mAudioTrack.getPlayState() + ",stopped= " + mStopped);
+ }
+
+ mIsShortUtterance = true;
+ mAudioTrack.stop();
+ }
+
+ // Block until the audio track is done only if we haven't stopped yet.
+ if (!mStopped) {
+ if (DBG) Log.d(TAG, "Waiting for audio track to complete : " + mAudioTrack.hashCode());
+ blockUntilDone(mAudioTrack);
+ }
+
+ // The last call to AudioTrack.write( ) will return only after
+ // all data from the audioTrack has been sent to the mixer, so
+ // it's safe to release at this point.
+ if (DBG) Log.d(TAG, "Releasing audio track [" + mAudioTrack.hashCode() + "]");
+ synchronized (mAudioTrackLock) {
+ mAudioTrack.release();
+ mAudioTrack = null;
+ }
+ }
+
+
+ static int getChannelConfig(int channelCount) {
+ if (channelCount == 1) {
+ return AudioFormat.CHANNEL_OUT_MONO;
+ } else if (channelCount == 2){
+ return AudioFormat.CHANNEL_OUT_STEREO;
+ }
+
+ return 0;
+ }
+
+ long getAudioLengthMs(int numBytes) {
+ final int unconsumedFrames = numBytes / mBytesPerFrame;
+ final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz;
+
+ return estimatedTimeMs;
+ }
+
+ private static int writeToAudioTrack(AudioTrack audioTrack, byte[] bytes) {
+ if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
+ if (DBG) Log.d(TAG, "AudioTrack not playing, restarting : " + audioTrack.hashCode());
+ audioTrack.play();
+ }
+
+ int count = 0;
+ while (count < bytes.length) {
+ // Note that we don't take bufferCopy.mOffset into account because
+ // it is guaranteed to be 0.
+ int written = audioTrack.write(bytes, count, bytes.length);
+ if (written <= 0) {
+ break;
+ }
+ count += written;
+ }
+ return count;
+ }
+
+ private AudioTrack createStreamingAudioTrack() {
+ final int channelConfig = getChannelConfig(mChannelCount);
+
+ int minBufferSizeInBytes
+ = AudioTrack.getMinBufferSize(mSampleRateInHz, channelConfig, mAudioFormat);
+ int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes);
+
+ AudioTrack audioTrack = new AudioTrack(mStreamType, mSampleRateInHz, channelConfig,
+ mAudioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
+ if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
+ Log.w(TAG, "Unable to create audio track.");
+ audioTrack.release();
+ return null;
+ }
+
+ mAudioBufferSize = bufferSizeInBytes;
+
+ setupVolume(audioTrack, mVolume, mPan);
+ return audioTrack;
+ }
+
+ private static int getBytesPerFrame(int audioFormat) {
+ if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) {
+ return 1;
+ } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
+ return 2;
+ }
+
+ return -1;
+ }
+
+
+ private void blockUntilDone(AudioTrack audioTrack) {
+ if (mBytesWritten <= 0) {
+ return;
+ }
+
+ if (mIsShortUtterance) {
+ // In this case we would have called AudioTrack#stop() to flush
+ // buffers to the mixer. This makes the playback head position
+ // unobservable and notification markers do not work reliably. We
+ // have no option but to wait until we think the track would finish
+ // playing and release it after.
+ //
+ // This isn't as bad as it looks because (a) We won't end up waiting
+ // for much longer than we should because even at 4khz mono, a short
+ // utterance weighs in at about 2 seconds, and (b) such short utterances
+ // are expected to be relatively infrequent and in a stream of utterances
+ // this shows up as a slightly longer pause.
+ blockUntilEstimatedCompletion();
+ } else {
+ blockUntilCompletion(audioTrack);
+ }
+ }
+
+ private void blockUntilEstimatedCompletion() {
+ final int lengthInFrames = mBytesWritten / mBytesPerFrame;
+ final long estimatedTimeMs = (lengthInFrames * 1000 / mSampleRateInHz);
+
+ if (DBG) Log.d(TAG, "About to sleep for: " + estimatedTimeMs + "ms for a short utterance");
+
+ try {
+ Thread.sleep(estimatedTimeMs);
+ } catch (InterruptedException ie) {
+ // Do nothing.
+ }
+ }
+
+ private void blockUntilCompletion(AudioTrack audioTrack) {
+ final int lengthInFrames = mBytesWritten / mBytesPerFrame;
+
+ int previousPosition = -1;
+ int currentPosition = 0;
+ long blockedTimeMs = 0;
+
+ while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames &&
+ audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && !mStopped) {
+
+ final long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) /
+ audioTrack.getSampleRate();
+ final long sleepTimeMs = clip(estimatedTimeMs, MIN_SLEEP_TIME_MS, MAX_SLEEP_TIME_MS);
+
+ // Check if the audio track has made progress since the last loop
+ // iteration. We should then add in the amount of time that was
+ // spent sleeping in the last iteration.
+ if (currentPosition == previousPosition) {
+ // This works only because the sleep time that would have been calculated
+ // would be the same in the previous iteration too.
+ blockedTimeMs += sleepTimeMs;
+ // If we've taken too long to make progress, bail.
+ if (blockedTimeMs > MAX_PROGRESS_WAIT_MS) {
+ Log.w(TAG, "Waited unsuccessfully for " + MAX_PROGRESS_WAIT_MS + "ms " +
+ "for AudioTrack to make progress, Aborting");
+ break;
+ }
+ } else {
+ blockedTimeMs = 0;
+ }
+ previousPosition = currentPosition;
+
+ if (DBG) {
+ Log.d(TAG, "About to sleep for : " + sleepTimeMs + " ms," +
+ " Playback position : " + currentPosition + ", Length in frames : "
+ + lengthInFrames);
+ }
+ try {
+ Thread.sleep(sleepTimeMs);
+ } catch (InterruptedException ie) {
+ break;
+ }
+ }
+ }
+
+ private static void setupVolume(AudioTrack audioTrack, float volume, float pan) {
+ final float vol = clip(volume, 0.0f, 1.0f);
+ final float panning = clip(pan, -1.0f, 1.0f);
+
+ float volLeft = vol;
+ float volRight = vol;
+ if (panning > 0.0f) {
+ volLeft *= (1.0f - panning);
+ } else if (panning < 0.0f) {
+ volRight *= (1.0f + panning);
+ }
+ if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight);
+ if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) {
+ Log.e(TAG, "Failed to set volume");
+ }
+ }
+
+ private static final long clip(long value, long min, long max) {
+ if (value < min) {
+ return min;
+ }
+
+ if (value > max) {
+ return max;
+ }
+
+ return value;
+ }
+
+ private static float clip(float value, float min, float max) {
+ return value > max ? max : (value < min ? min : value);
+ }
+
+}
diff --git a/core/java/android/speech/tts/EventLogTags.logtags b/core/java/android/speech/tts/EventLogTags.logtags
index 1a9f5fe..f8654ad 100644
--- a/core/java/android/speech/tts/EventLogTags.logtags
+++ b/core/java/android/speech/tts/EventLogTags.logtags
@@ -2,5 +2,5 @@
option java_package android.speech.tts;
-76001 tts_speak_success (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3)
-76002 tts_speak_failure (engine|3),(caller|3),(length|1),(locale|3),(rate|1),(pitch|1)
+76001 tts_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3)
+76002 tts_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1)
diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java
index 63b954b..82ed4dd 100644
--- a/core/java/android/speech/tts/EventLogger.java
+++ b/core/java/android/speech/tts/EventLogger.java
@@ -17,6 +17,7 @@ package android.speech.tts;
import android.os.SystemClock;
import android.text.TextUtils;
+import android.util.Log;
/**
* Writes data about a given speech synthesis request to the event logs.
@@ -24,14 +25,15 @@ import android.text.TextUtils;
* speech rate / pitch and the latency and overall time taken.
*
* Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()}
- * might be called from any thread, but on {@link EventLogger#onPlaybackStart()} and
+ * might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and
* {@link EventLogger#onComplete()} must be called from a single thread
* (usually the audio playback thread}
*/
class EventLogger {
private final SynthesisRequest mRequest;
- private final String mCallingApp;
private final String mServiceApp;
+ private final int mCallerUid;
+ private final int mCallerPid;
private final long mReceivedTime;
private long mPlaybackStartTime = -1;
private volatile long mRequestProcessingStartTime = -1;
@@ -42,10 +44,10 @@ class EventLogger {
private volatile boolean mStopped = false;
private boolean mLogWritten = false;
- EventLogger(SynthesisRequest request, String callingApp,
- String serviceApp) {
+ EventLogger(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) {
mRequest = request;
- mCallingApp = callingApp;
+ mCallerUid = callerUid;
+ mCallerPid = callerPid;
mServiceApp = serviceApp;
mReceivedTime = SystemClock.elapsedRealtime();
}
@@ -80,10 +82,10 @@ class EventLogger {
/**
* Notifies the logger that audio playback has started for some section
* of the synthesis. This is normally some amount of time after the engine
- * has synthesized data and varides depending on utterances and
+ * has synthesized data and varies depending on utterances and
* other audio currently in the queue.
*/
- public void onPlaybackStart() {
+ public void onAudioDataWritten() {
// For now, keep track of only the first chunk of audio
// that was played.
if (mPlaybackStartTime == -1) {
@@ -119,10 +121,10 @@ class EventLogger {
}
long completionTime = SystemClock.elapsedRealtime();
- // onPlaybackStart() should normally always be called if an
+ // onAudioDataWritten() should normally always be called if an
// error does not occur.
if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) {
- EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallingApp,
+ EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid,
getUtteranceLength(), getLocaleString(),
mRequest.getSpeechRate(), mRequest.getPitch());
return;
@@ -138,7 +140,8 @@ class EventLogger {
final long audioLatency = mPlaybackStartTime - mReceivedTime;
final long engineLatency = mEngineStartTime - mRequestProcessingStartTime;
final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime;
- EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallingApp,
+
+ EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid,
getUtteranceLength(), getLocaleString(),
mRequest.getSpeechRate(), mRequest.getPitch(),
engineLatency, engineTotal, audioLatency);
diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl
index 1a8c1fb..ab63187 100644
--- a/core/java/android/speech/tts/ITextToSpeechService.aidl
+++ b/core/java/android/speech/tts/ITextToSpeechService.aidl
@@ -30,47 +30,47 @@ interface ITextToSpeechService {
/**
* Tells the engine to synthesize some speech and play it back.
*
- * @param callingApp The package name of the calling app. Used to connect requests
- * callbacks and to clear requests when the calling app is stopping.
+ * @param callingInstance a binder representing the identity of the calling
+ * TextToSpeech object.
* @param text The text to synthesize.
* @param queueMode Determines what to do to requests already in the queue.
* @param param Request parameters.
*/
- int speak(in String callingApp, in String text, in int queueMode, in Bundle params);
+ int speak(in IBinder callingInstance, in String text, in int queueMode, in Bundle params);
/**
* Tells the engine to synthesize some speech and write it to a file.
*
- * @param callingApp The package name of the calling app. Used to connect requests
- * callbacks and to clear requests when the calling app is stopping.
+ * @param callingInstance a binder representing the identity of the calling
+ * TextToSpeech object.
* @param text The text to synthesize.
* @param filename The file to write the synthesized audio to.
* @param param Request parameters.
*/
- int synthesizeToFile(in String callingApp, in String text,
+ int synthesizeToFile(in IBinder callingInstance, in String text,
in String filename, in Bundle params);
/**
* Plays an existing audio resource.
*
- * @param callingApp The package name of the calling app. Used to connect requests
- * callbacks and to clear requests when the calling app is stopping.
+ * @param callingInstance a binder representing the identity of the calling
+ * TextToSpeech object.
* @param audioUri URI for the audio resource (a file or android.resource URI)
* @param queueMode Determines what to do to requests already in the queue.
* @param param Request parameters.
*/
- int playAudio(in String callingApp, in Uri audioUri, in int queueMode, in Bundle params);
+ int playAudio(in IBinder callingInstance, in Uri audioUri, in int queueMode, in Bundle params);
/**
* Plays silence.
*
- * @param callingApp The package name of the calling app. Used to connect requests
- * callbacks and to clear requests when the calling app is stopping.
+ * @param callingInstance a binder representing the identity of the calling
+ * TextToSpeech object.
* @param duration Number of milliseconds of silence to play.
* @param queueMode Determines what to do to requests already in the queue.
* @param param Request parameters.
*/
- int playSilence(in String callingApp, in long duration, in int queueMode, in Bundle params);
+ int playSilence(in IBinder callingInstance, in long duration, in int queueMode, in Bundle params);
/**
* Checks whether the service is currently playing some audio.
@@ -81,10 +81,10 @@ interface ITextToSpeechService {
* Interrupts the current utterance (if from the given app) and removes any utterances
* in the queue that are from the given app.
*
- * @param callingApp Package name of the app whose utterances
- * should be interrupted and cleared.
+ * @param callingInstance a binder representing the identity of the calling
+ * TextToSpeech object.
*/
- int stop(in String callingApp);
+ int stop(in IBinder callingInstance);
/**
* Returns the language, country and variant currently being used by the TTS engine.
@@ -150,6 +150,6 @@ interface ITextToSpeechService {
* @param callingApp Package name for the app whose utterance the callback will handle.
* @param cb The callback.
*/
- void setCallback(in String callingApp, ITextToSpeechCallback cb);
+ void setCallback(in IBinder caller, ITextToSpeechCallback cb);
}
diff --git a/core/java/android/speech/tts/MessageParams.java b/core/java/android/speech/tts/MessageParams.java
deleted file mode 100644
index de9cc07..0000000
--- a/core/java/android/speech/tts/MessageParams.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.speech.tts;
-
-import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
-
-abstract class MessageParams {
- static final int TYPE_SYNTHESIS = 1;
- static final int TYPE_AUDIO = 2;
- static final int TYPE_SILENCE = 3;
-
- private final UtteranceProgressDispatcher mDispatcher;
- private final String mCallingApp;
-
- MessageParams(UtteranceProgressDispatcher dispatcher, String callingApp) {
- mDispatcher = dispatcher;
- mCallingApp = callingApp;
- }
-
- UtteranceProgressDispatcher getDispatcher() {
- return mDispatcher;
- }
-
- String getCallingApp() {
- return mCallingApp;
- }
-
- @Override
- public String toString() {
- return "MessageParams[" + hashCode() + "]";
- }
-
- abstract int getType();
-}
diff --git a/core/java/android/speech/tts/PlaybackQueueItem.java b/core/java/android/speech/tts/PlaybackQueueItem.java
new file mode 100644
index 0000000..d0957ff
--- /dev/null
+++ b/core/java/android/speech/tts/PlaybackQueueItem.java
@@ -0,0 +1,27 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package android.speech.tts;
+
+import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
+
+abstract class PlaybackQueueItem implements Runnable {
+ private final UtteranceProgressDispatcher mDispatcher;
+ private final Object mCallerIdentity;
+
+ PlaybackQueueItem(TextToSpeechService.UtteranceProgressDispatcher dispatcher,
+ Object callerIdentity) {
+ mDispatcher = dispatcher;
+ mCallerIdentity = callerIdentity;
+ }
+
+ Object getCallerIdentity() {
+ return mCallerIdentity;
+ }
+
+ protected UtteranceProgressDispatcher getDispatcher() {
+ return mDispatcher;
+ }
+
+ public abstract void run();
+ abstract void stop(boolean isError);
+}
diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java
index 91a3452..c99f201 100644
--- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java
+++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java
@@ -47,34 +47,34 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
private final float mPan;
/**
- * Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}.
+ * Guards {@link #mAudioTrackHandler}, {@link #mItem} and {@link #mStopped}.
*/
private final Object mStateLock = new Object();
// Handler associated with a thread that plays back audio requests.
private final AudioPlaybackHandler mAudioTrackHandler;
// A request "token", which will be non null after start() has been called.
- private SynthesisMessageParams mToken = null;
+ private SynthesisPlaybackQueueItem mItem = null;
// Whether this request has been stopped. This is useful for keeping
// track whether stop() has been called before start(). In all other cases,
- // a non-null value of mToken will provide the same information.
+ // a non-null value of mItem will provide the same information.
private boolean mStopped = false;
private volatile boolean mDone = false;
private final UtteranceProgressDispatcher mDispatcher;
- private final String mCallingApp;
+ private final Object mCallerIdentity;
private final EventLogger mLogger;
PlaybackSynthesisCallback(int streamType, float volume, float pan,
AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher,
- String callingApp, EventLogger logger) {
+ Object callerIdentity, EventLogger logger) {
mStreamType = streamType;
mVolume = volume;
mPan = pan;
mAudioTrackHandler = audioTrackHandler;
mDispatcher = dispatcher;
- mCallingApp = callingApp;
+ mCallerIdentity = callerIdentity;
mLogger = logger;
}
@@ -89,28 +89,23 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
// Note that mLogger.mError might be true too at this point.
mLogger.onStopped();
- SynthesisMessageParams token;
+ SynthesisPlaybackQueueItem item;
synchronized (mStateLock) {
if (mStopped) {
Log.w(TAG, "stop() called twice");
return;
}
- token = mToken;
+ item = mItem;
mStopped = true;
}
- if (token != null) {
+ if (item != null) {
// This might result in the synthesis thread being woken up, at which
- // point it will write an additional buffer to the token - but we
+ // point it will write an additional buffer to the item - but we
// won't worry about that because the audio playback queue will be cleared
// soon after (see SynthHandler#stop(String).
- token.setIsError(wasError);
- token.clearBuffers();
- if (wasError) {
- // Also clean up the audio track if an error occurs.
- mAudioTrackHandler.enqueueSynthesisDone(token);
- }
+ item.stop(wasError);
} else {
// This happens when stop() or error() were called before start() was.
@@ -145,7 +140,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
+ "," + channelCount + ")");
}
- int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount);
+ int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);
if (channelConfig == 0) {
Log.e(TAG, "Unsupported number of channels :" + channelCount);
return TextToSpeech.ERROR;
@@ -156,12 +151,11 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
if (DBG) Log.d(TAG, "stop() called before start(), returning.");
return TextToSpeech.ERROR;
}
- SynthesisMessageParams params = new SynthesisMessageParams(
+ SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan,
- mDispatcher, mCallingApp, mLogger);
- mAudioTrackHandler.enqueueSynthesisStart(params);
-
- mToken = params;
+ mDispatcher, mCallerIdentity, mLogger);
+ mAudioTrackHandler.enqueue(item);
+ mItem = item;
}
return TextToSpeech.SUCCESS;
@@ -179,21 +173,25 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
+ length + " bytes)");
}
- SynthesisMessageParams token = null;
+ SynthesisPlaybackQueueItem item = null;
synchronized (mStateLock) {
- if (mToken == null || mStopped) {
+ if (mItem == null || mStopped) {
return TextToSpeech.ERROR;
}
- token = mToken;
+ item = mItem;
}
// Sigh, another copy.
final byte[] bufferCopy = new byte[length];
System.arraycopy(buffer, offset, bufferCopy, 0, length);
- // Might block on mToken.this, if there are too many buffers waiting to
+
+ // Might block on mItem.this, if there are too many buffers waiting to
// be consumed.
- token.addBuffer(bufferCopy);
- mAudioTrackHandler.enqueueSynthesisDataAvailable(token);
+ try {
+ item.put(bufferCopy);
+ } catch (InterruptedException ie) {
+ return TextToSpeech.ERROR;
+ }
mLogger.onEngineDataReceived();
@@ -204,7 +202,7 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
public int done() {
if (DBG) Log.d(TAG, "done()");
- SynthesisMessageParams token = null;
+ SynthesisPlaybackQueueItem item = null;
synchronized (mStateLock) {
if (mDone) {
Log.w(TAG, "Duplicate call to done()");
@@ -213,14 +211,14 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
mDone = true;
- if (mToken == null) {
+ if (mItem == null) {
return TextToSpeech.ERROR;
}
- token = mToken;
+ item = mItem;
}
- mAudioTrackHandler.enqueueSynthesisDone(token);
+ item.done();
mLogger.onEngineComplete();
return TextToSpeech.SUCCESS;
diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilencePlaybackQueueItem.java
index 9909126..a5e47ae 100644
--- a/core/java/android/speech/tts/SilenceMessageParams.java
+++ b/core/java/android/speech/tts/SilencePlaybackQueueItem.java
@@ -17,28 +17,29 @@ package android.speech.tts;
import android.os.ConditionVariable;
import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
+import android.util.Log;
-class SilenceMessageParams extends MessageParams {
+class SilencePlaybackQueueItem extends PlaybackQueueItem {
private final ConditionVariable mCondVar = new ConditionVariable();
private final long mSilenceDurationMs;
- SilenceMessageParams(UtteranceProgressDispatcher dispatcher,
- String callingApp, long silenceDurationMs) {
- super(dispatcher, callingApp);
+ SilencePlaybackQueueItem(UtteranceProgressDispatcher dispatcher,
+ Object callerIdentity, long silenceDurationMs) {
+ super(dispatcher, callerIdentity);
mSilenceDurationMs = silenceDurationMs;
}
- long getSilenceDurationMs() {
- return mSilenceDurationMs;
- }
-
@Override
- int getType() {
- return TYPE_SILENCE;
+ public void run() {
+ getDispatcher().dispatchOnStart();
+ if (mSilenceDurationMs > 0) {
+ mCondVar.block(mSilenceDurationMs);
+ }
+ getDispatcher().dispatchOnDone();
}
- ConditionVariable getConditionVariable() {
- return mCondVar;
+ @Override
+ void stop(boolean isError) {
+ mCondVar.open();
}
-
}
diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java
deleted file mode 100644
index ed66420..0000000
--- a/core/java/android/speech/tts/SynthesisMessageParams.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.speech.tts;
-
-import android.media.AudioFormat;
-import android.media.AudioTrack;
-import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
-
-import java.util.LinkedList;
-
-/**
- * Params required to play back a synthesis request.
- */
-final class SynthesisMessageParams extends MessageParams {
- private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
-
- final int mStreamType;
- final int mSampleRateInHz;
- final int mAudioFormat;
- final int mChannelCount;
- final float mVolume;
- final float mPan;
- final EventLogger mLogger;
-
- final int mBytesPerFrame;
-
- volatile AudioTrack mAudioTrack;
- // Written by the synthesis thread, but read on the audio playback
- // thread.
- volatile int mBytesWritten;
- // A "short utterance" is one that uses less bytes than the audio
- // track buffer size (mAudioBufferSize). In this case, we need to call
- // AudioTrack#stop() to send pending buffers to the mixer, and slightly
- // different logic is required to wait for the track to finish.
- //
- // Not volatile, accessed only from the audio playback thread.
- boolean mIsShortUtterance;
- int mAudioBufferSize;
- // Always synchronized on "this".
- int mUnconsumedBytes;
- volatile boolean mIsError;
-
- private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
-
- SynthesisMessageParams(int streamType, int sampleRate,
- int audioFormat, int channelCount,
- float volume, float pan, UtteranceProgressDispatcher dispatcher,
- String callingApp, EventLogger logger) {
- super(dispatcher, callingApp);
-
- mStreamType = streamType;
- mSampleRateInHz = sampleRate;
- mAudioFormat = audioFormat;
- mChannelCount = channelCount;
- mVolume = volume;
- mPan = pan;
- mLogger = logger;
-
- mBytesPerFrame = getBytesPerFrame(mAudioFormat) * mChannelCount;
-
- // initially null.
- mAudioTrack = null;
- mBytesWritten = 0;
- mAudioBufferSize = 0;
- mIsError = false;
- }
-
- @Override
- int getType() {
- return TYPE_SYNTHESIS;
- }
-
- synchronized void addBuffer(byte[] buffer) {
- long unconsumedAudioMs = 0;
-
- while ((unconsumedAudioMs = getUnconsumedAudioLengthMs()) > MAX_UNCONSUMED_AUDIO_MS) {
- try {
- wait();
- } catch (InterruptedException ie) {
- return;
- }
- }
-
- mDataBufferList.add(new ListEntry(buffer));
- mUnconsumedBytes += buffer.length;
- }
-
- synchronized void clearBuffers() {
- mDataBufferList.clear();
- mUnconsumedBytes = 0;
- notifyAll();
- }
-
- synchronized ListEntry getNextBuffer() {
- ListEntry entry = mDataBufferList.poll();
- if (entry != null) {
- mUnconsumedBytes -= entry.mBytes.length;
- notifyAll();
- }
-
- return entry;
- }
-
- void setAudioTrack(AudioTrack audioTrack) {
- mAudioTrack = audioTrack;
- }
-
- AudioTrack getAudioTrack() {
- return mAudioTrack;
- }
-
- void setIsError(boolean isError) {
- mIsError = isError;
- }
-
- boolean isError() {
- return mIsError;
- }
-
- // Must be called synchronized on this.
- private long getUnconsumedAudioLengthMs() {
- final int unconsumedFrames = mUnconsumedBytes / mBytesPerFrame;
- final long estimatedTimeMs = unconsumedFrames * 1000 / mSampleRateInHz;
-
- return estimatedTimeMs;
- }
-
- private static int getBytesPerFrame(int audioFormat) {
- if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) {
- return 1;
- } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) {
- return 2;
- }
-
- return -1;
- }
-
- static final class ListEntry {
- final byte[] mBytes;
-
- ListEntry(byte[] bytes) {
- mBytes = bytes;
- }
- }
-}
-
diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
new file mode 100644
index 0000000..d299d70
--- /dev/null
+++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.speech.tts;
+
+import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher;
+import android.util.Log;
+
+import java.util.LinkedList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Manages the playback of a list of byte arrays representing audio data
+ * that are queued by the engine to an audio track.
+ */
+final class SynthesisPlaybackQueueItem extends PlaybackQueueItem {
+ private static final String TAG = "TTS.SynthQueueItem";
+ private static final boolean DBG = false;
+
+ /**
+ * Maximum length of audio we leave unconsumed by the audio track.
+ * Calls to {@link #put(byte[])} will block until we have less than
+ * this amount of audio left to play back.
+ */
+ private static final long MAX_UNCONSUMED_AUDIO_MS = 500;
+
+ /**
+ * Guards accesses to mDataBufferList and mUnconsumedBytes.
+ */
+ private final Lock mListLock = new ReentrantLock();
+ private final Condition mReadReady = mListLock.newCondition();
+ private final Condition mNotFull = mListLock.newCondition();
+
+ // Guarded by mListLock.
+ private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>();
+ // Guarded by mListLock.
+ private int mUnconsumedBytes;
+
+ /*
+ * While mStopped and mIsError can be written from any thread, mDone is written
+ * only from the synthesis thread. All three variables are read from the
+ * audio playback thread.
+ */
+ private volatile boolean mStopped;
+ private volatile boolean mDone;
+ private volatile boolean mIsError;
+
+ private final BlockingAudioTrack mAudioTrack;
+ private final EventLogger mLogger;
+
+
+ SynthesisPlaybackQueueItem(int streamType, int sampleRate,
+ int audioFormat, int channelCount,
+ float volume, float pan, UtteranceProgressDispatcher dispatcher,
+ Object callerIdentity, EventLogger logger) {
+ super(dispatcher, callerIdentity);
+
+ mUnconsumedBytes = 0;
+
+ mStopped = false;
+ mDone = false;
+ mIsError = false;
+
+ mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat,
+ channelCount, volume, pan);
+ mLogger = logger;
+ }
+
+
+ @Override
+ public void run() {
+ final UtteranceProgressDispatcher dispatcher = getDispatcher();
+ dispatcher.dispatchOnStart();
+
+
+ mAudioTrack.init();
+
+ try {
+ byte[] buffer = null;
+
+ // take() will block until:
+ //
+ // (a) there is a buffer available to tread. In which case
+ // a non null value is returned.
+ // OR (b) stop() is called in which case it will return null.
+ // OR (c) done() is called in which case it will return null.
+ while ((buffer = take()) != null) {
+ mAudioTrack.write(buffer);
+ mLogger.onAudioDataWritten();
+ }
+
+ } catch (InterruptedException ie) {
+ if (DBG) Log.d(TAG, "Interrupted waiting for buffers, cleaning up.");
+ }
+
+ mAudioTrack.waitAndRelease();
+
+ if (mIsError) {
+ dispatcher.dispatchOnError();
+ } else {
+ dispatcher.dispatchOnDone();
+ }
+
+ mLogger.onWriteData();
+ }
+
+ @Override
+ void stop(boolean isError) {
+ try {
+ mListLock.lock();
+
+ // Update our internal state.
+ mStopped = true;
+ mIsError = isError;
+
+ // Wake up the audio playback thread if it was waiting on take().
+ // take() will return null since mStopped was true, and will then
+ // break out of the data write loop.
+ mReadReady.signal();
+
+ // Wake up the synthesis thread if it was waiting on put(). Its
+ // buffers will no longer be copied since mStopped is true. The
+ // PlaybackSynthesisCallback that this synthesis corresponds to
+ // would also have been stopped, and so all calls to
+ // Callback.onDataAvailable( ) will return errors too.
+ mNotFull.signal();
+ } finally {
+ mListLock.unlock();
+ }
+
+ // Stop the underlying audio track. This will stop sending
+ // data to the mixer and discard any pending buffers that the
+ // track holds.
+ mAudioTrack.stop();
+ }
+
+ void done() {
+ try {
+ mListLock.lock();
+
+ // Update state.
+ mDone = true;
+
+ // Unblocks the audio playback thread if it was waiting on take()
+ // after having consumed all available buffers. It will then return
+ // null and leave the write loop.
+ mReadReady.signal();
+
+ // Just so that engines that try to queue buffers after
+ // calling done() don't block the synthesis thread forever. Ideally
+ // this should be called from the same thread as put() is, and hence
+ // this call should be pointless.
+ mNotFull.signal();
+ } finally {
+ mListLock.unlock();
+ }
+ }
+
+
+ void put(byte[] buffer) throws InterruptedException {
+ try {
+ mListLock.lock();
+ long unconsumedAudioMs = 0;
+
+ while ((unconsumedAudioMs = mAudioTrack.getAudioLengthMs(mUnconsumedBytes)) >
+ MAX_UNCONSUMED_AUDIO_MS && !mStopped) {
+ mNotFull.await();
+ }
+
+ // Don't bother queueing the buffer if we've stopped. The playback thread
+ // would have woken up when stop() is called (if it was blocked) and will
+ // proceed to leave the write loop since take() will return null when
+ // stopped.
+ if (mStopped) {
+ return;
+ }
+
+ mDataBufferList.add(new ListEntry(buffer));
+ mUnconsumedBytes += buffer.length;
+ mReadReady.signal();
+ } finally {
+ mListLock.unlock();
+ }
+ }
+
+ private byte[] take() throws InterruptedException {
+ try {
+ mListLock.lock();
+
+ // Block if there are no available buffers, and stop() has not
+ // been called and done() has not been called.
+ while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
+ mReadReady.await();
+ }
+
+ // If stopped, return null so that we can exit the playback loop
+ // as soon as possible.
+ if (mStopped) {
+ return null;
+ }
+
+ // Remove the first entry from the queue.
+ ListEntry entry = mDataBufferList.poll();
+
+ // This is the normal playback loop exit case, when done() was
+ // called. (mDone will be true at this point).
+ if (entry == null) {
+ return null;
+ }
+
+ mUnconsumedBytes -= entry.mBytes.length;
+ // Unblock the waiting writer. We use signal() and not signalAll()
+ // because there will only be one thread waiting on this (the
+ // Synthesis thread).
+ mNotFull.signal();
+
+ return entry.mBytes;
+ } finally {
+ mListLock.unlock();
+ }
+ }
+
+ static final class ListEntry {
+ final byte[] mBytes;
+
+ ListEntry(byte[] bytes) {
+ mBytes = bytes;
+ }
+ }
+}
+
diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java
index a220615..7a174af 100755
--- a/core/java/android/speech/tts/TextToSpeech.java
+++ b/core/java/android/speech/tts/TextToSpeech.java
@@ -486,6 +486,11 @@ public class TextToSpeech {
private final Object mStartLock = new Object();
private String mRequestedEngine;
+ // Whether to initialize this TTS object with the default engine,
+ // if the requested engine is not available. Valid only if mRequestedEngine
+ // is not null. Used only for testing, though potentially useful API wise
+ // too.
+ private final boolean mUseFallback;
private final Map<String, Uri> mEarcons;
private final Map<String, Uri> mUtterances;
private final Bundle mParams = new Bundle();
@@ -519,7 +524,7 @@ public class TextToSpeech {
* @param engine Package name of the TTS engine to use.
*/
public TextToSpeech(Context context, OnInitListener listener, String engine) {
- this(context, listener, engine, null);
+ this(context, listener, engine, null, true);
}
/**
@@ -529,10 +534,11 @@ public class TextToSpeech {
* @hide
*/
public TextToSpeech(Context context, OnInitListener listener, String engine,
- String packageName) {
+ String packageName, boolean useFallback) {
mContext = context;
mInitListener = listener;
mRequestedEngine = engine;
+ mUseFallback = useFallback;
mEarcons = new HashMap<String, Uri>();
mUtterances = new HashMap<String, Uri>();
@@ -547,10 +553,6 @@ public class TextToSpeech {
initTts();
}
- private String getPackageName() {
- return mPackageName;
- }
-
private <R> R runActionNoReconnect(Action<R> action, R errorResult, String method) {
return runAction(action, errorResult, method, false);
}
@@ -571,10 +573,21 @@ public class TextToSpeech {
private int initTts() {
// Step 1: Try connecting to the engine that was requested.
- if (mRequestedEngine != null && mEnginesHelper.isEngineInstalled(mRequestedEngine)) {
- if (connectToEngine(mRequestedEngine)) {
- mCurrentEngine = mRequestedEngine;
- return SUCCESS;
+ if (mRequestedEngine != null) {
+ if (mEnginesHelper.isEngineInstalled(mRequestedEngine)) {
+ if (connectToEngine(mRequestedEngine)) {
+ mCurrentEngine = mRequestedEngine;
+ return SUCCESS;
+ } else if (!mUseFallback) {
+ mCurrentEngine = null;
+ dispatchOnInit(ERROR);
+ return ERROR;
+ }
+ } else if (!mUseFallback) {
+ Log.i(TAG, "Requested engine not installed: " + mRequestedEngine);
+ mCurrentEngine = null;
+ dispatchOnInit(ERROR);
+ return ERROR;
}
}
@@ -630,6 +643,10 @@ public class TextToSpeech {
}
}
+ private IBinder getCallerIdentity() {
+ return mServiceConnection.getCallerIdentity();
+ }
+
/**
* Releases the resources used by the TextToSpeech engine.
* It is good practice for instance to call this method in the onDestroy() method of an Activity
@@ -639,8 +656,8 @@ public class TextToSpeech {
runActionNoReconnect(new Action<Void>() {
@Override
public Void run(ITextToSpeechService service) throws RemoteException {
- service.setCallback(getPackageName(), null);
- service.stop(getPackageName());
+ service.setCallback(getCallerIdentity(), null);
+ service.stop(getCallerIdentity());
mServiceConnection.disconnect();
// Context#unbindService does not result in a call to
// ServiceConnection#onServiceDisconnected. As a result, the
@@ -800,10 +817,10 @@ public class TextToSpeech {
public Integer run(ITextToSpeechService service) throws RemoteException {
Uri utteranceUri = mUtterances.get(text);
if (utteranceUri != null) {
- return service.playAudio(getPackageName(), utteranceUri, queueMode,
+ return service.playAudio(getCallerIdentity(), utteranceUri, queueMode,
getParams(params));
} else {
- return service.speak(getPackageName(), text, queueMode, getParams(params));
+ return service.speak(getCallerIdentity(), text, queueMode, getParams(params));
}
}
}, ERROR, "speak");
@@ -836,7 +853,7 @@ public class TextToSpeech {
if (earconUri == null) {
return ERROR;
}
- return service.playAudio(getPackageName(), earconUri, queueMode,
+ return service.playAudio(getCallerIdentity(), earconUri, queueMode,
getParams(params));
}
}, ERROR, "playEarcon");
@@ -863,7 +880,7 @@ public class TextToSpeech {
return runAction(new Action<Integer>() {
@Override
public Integer run(ITextToSpeechService service) throws RemoteException {
- return service.playSilence(getPackageName(), durationInMs, queueMode,
+ return service.playSilence(getCallerIdentity(), durationInMs, queueMode,
getParams(params));
}
}, ERROR, "playSilence");
@@ -926,7 +943,7 @@ public class TextToSpeech {
return runAction(new Action<Integer>() {
@Override
public Integer run(ITextToSpeechService service) throws RemoteException {
- return service.stop(getPackageName());
+ return service.stop(getCallerIdentity());
}
}, ERROR, "stop");
}
@@ -1091,7 +1108,7 @@ public class TextToSpeech {
return runAction(new Action<Integer>() {
@Override
public Integer run(ITextToSpeechService service) throws RemoteException {
- return service.synthesizeToFile(getPackageName(), text, filename,
+ return service.synthesizeToFile(getCallerIdentity(), text, filename,
getParams(params));
}
}, ERROR, "synthesizeToFile");
@@ -1275,7 +1292,7 @@ public class TextToSpeech {
mServiceConnection = this;
mService = ITextToSpeechService.Stub.asInterface(service);
try {
- mService.setCallback(getPackageName(), mCallback);
+ mService.setCallback(getCallerIdentity(), mCallback);
dispatchOnInit(SUCCESS);
} catch (RemoteException re) {
Log.e(TAG, "Error connecting to service, setCallback() failed");
@@ -1284,6 +1301,10 @@ public class TextToSpeech {
}
}
+ public IBinder getCallerIdentity() {
+ return mCallback;
+ }
+
public void onServiceDisconnected(ComponentName name) {
synchronized(mStartLock) {
mService = null;
diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java
index aee678a..4c1a0af 100644
--- a/core/java/android/speech/tts/TextToSpeechService.java
+++ b/core/java/android/speech/tts/TextToSpeechService.java
@@ -18,6 +18,7 @@ package android.speech.tts;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -272,9 +273,9 @@ public abstract class TextToSpeechService extends Service {
return old;
}
- private synchronized SpeechItem maybeRemoveCurrentSpeechItem(String callingApp) {
+ private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
if (mCurrentSpeechItem != null &&
- TextUtils.equals(mCurrentSpeechItem.getCallingApp(), callingApp)) {
+ mCurrentSpeechItem.getCallerIdentity() == callerIdentity) {
SpeechItem current = mCurrentSpeechItem;
mCurrentSpeechItem = null;
return current;
@@ -311,7 +312,7 @@ public abstract class TextToSpeechService extends Service {
}
if (queueMode == TextToSpeech.QUEUE_FLUSH) {
- stopForApp(speechItem.getCallingApp());
+ stopForApp(speechItem.getCallerIdentity());
} else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
stopAll();
}
@@ -328,7 +329,7 @@ public abstract class TextToSpeechService extends Service {
// stopForApp(String).
//
// Note that this string is interned, so the == comparison works.
- msg.obj = speechItem.getCallingApp();
+ msg.obj = speechItem.getCallerIdentity();
if (sendMessage(msg)) {
return TextToSpeech.SUCCESS;
} else {
@@ -344,12 +345,12 @@ public abstract class TextToSpeechService extends Service {
*
* Called on a service binder thread.
*/
- public int stopForApp(String callingApp) {
- if (TextUtils.isEmpty(callingApp)) {
+ public int stopForApp(Object callerIdentity) {
+ if (callerIdentity == null) {
return TextToSpeech.ERROR;
}
- removeCallbacksAndMessages(callingApp);
+ removeCallbacksAndMessages(callerIdentity);
// This stops writing data to the file / or publishing
// items to the audio playback handler.
//
@@ -357,13 +358,13 @@ public abstract class TextToSpeechService extends Service {
// belongs to the callingApp, else the item will be "orphaned" and
// not stopped correctly if a stop request comes along for the item
// from the app it belongs to.
- SpeechItem current = maybeRemoveCurrentSpeechItem(callingApp);
+ SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
if (current != null) {
current.stop();
}
// Remove any enqueued audio too.
- mAudioPlaybackHandler.removePlaybackItems(callingApp);
+ mAudioPlaybackHandler.stopForApp(callerIdentity);
return TextToSpeech.SUCCESS;
}
@@ -377,7 +378,7 @@ public abstract class TextToSpeechService extends Service {
// Remove all other items from the queue.
removeCallbacksAndMessages(null);
// Remove all pending playback as well.
- mAudioPlaybackHandler.removeAllItems();
+ mAudioPlaybackHandler.stop();
return TextToSpeech.SUCCESS;
}
@@ -393,18 +394,22 @@ public abstract class TextToSpeechService extends Service {
* An item in the synth thread queue.
*/
private abstract class SpeechItem implements UtteranceProgressDispatcher {
- private final String mCallingApp;
+ private final Object mCallerIdentity;
protected final Bundle mParams;
+ private final int mCallerUid;
+ private final int mCallerPid;
private boolean mStarted = false;
private boolean mStopped = false;
- public SpeechItem(String callingApp, Bundle params) {
- mCallingApp = callingApp;
+ public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) {
+ mCallerIdentity = caller;
mParams = params;
+ mCallerUid = callerUid;
+ mCallerPid = callerPid;
}
- public String getCallingApp() {
- return mCallingApp;
+ public Object getCallerIdentity() {
+ return mCallerIdentity;
}
/**
@@ -451,7 +456,7 @@ public abstract class TextToSpeechService extends Service {
public void dispatchOnDone() {
final String utteranceId = getUtteranceId();
if (utteranceId != null) {
- mCallbacks.dispatchOnDone(getCallingApp(), utteranceId);
+ mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId);
}
}
@@ -459,7 +464,7 @@ public abstract class TextToSpeechService extends Service {
public void dispatchOnStart() {
final String utteranceId = getUtteranceId();
if (utteranceId != null) {
- mCallbacks.dispatchOnStart(getCallingApp(), utteranceId);
+ mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
}
}
@@ -467,10 +472,18 @@ public abstract class TextToSpeechService extends Service {
public void dispatchOnError() {
final String utteranceId = getUtteranceId();
if (utteranceId != null) {
- mCallbacks.dispatchOnError(getCallingApp(), utteranceId);
+ mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId);
}
}
+ public int getCallerUid() {
+ return mCallerUid;
+ }
+
+ public int getCallerPid() {
+ return mCallerPid;
+ }
+
protected synchronized boolean isStopped() {
return mStopped;
}
@@ -518,13 +531,15 @@ public abstract class TextToSpeechService extends Service {
private AbstractSynthesisCallback mSynthesisCallback;
private final EventLogger mEventLogger;
- public SynthesisSpeechItem(String callingApp, Bundle params, String text) {
- super(callingApp, params);
+ public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid,
+ Bundle params, String text) {
+ super(callerIdentity, callerUid, callerPid, params);
mText = text;
mSynthesisRequest = new SynthesisRequest(mText, mParams);
mDefaultLocale = getSettingsLocale();
setRequestParams(mSynthesisRequest);
- mEventLogger = new EventLogger(mSynthesisRequest, getCallingApp(), mPackageName);
+ mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid,
+ mPackageName);
}
public String getText() {
@@ -563,7 +578,7 @@ public abstract class TextToSpeechService extends Service {
protected AbstractSynthesisCallback createSynthesisCallback() {
return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(),
- mAudioPlaybackHandler, this, getCallingApp(), mEventLogger);
+ mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger);
}
private void setRequestParams(SynthesisRequest request) {
@@ -618,9 +633,10 @@ public abstract class TextToSpeechService extends Service {
private class SynthesisToFileSpeechItem extends SynthesisSpeechItem {
private final File mFile;
- public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text,
+ public SynthesisToFileSpeechItem(Object callerIdentity, int callerUid, int callerPid,
+ Bundle params, String text,
File file) {
- super(callingApp, params, text);
+ super(callerIdentity, callerUid, callerPid, params, text);
mFile = file;
}
@@ -678,13 +694,12 @@ public abstract class TextToSpeechService extends Service {
}
private class AudioSpeechItem extends SpeechItem {
-
- private final BlockingMediaPlayer mPlayer;
- private AudioMessageParams mToken;
-
- public AudioSpeechItem(String callingApp, Bundle params, Uri uri) {
- super(callingApp, params);
- mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType());
+ private final AudioPlaybackQueueItem mItem;
+ public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid,
+ Bundle params, Uri uri) {
+ super(callerIdentity, callerUid, callerPid, params);
+ mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
+ TextToSpeechService.this, uri, getStreamType());
}
@Override
@@ -694,8 +709,7 @@ public abstract class TextToSpeechService extends Service {
@Override
protected int playImpl() {
- mToken = new AudioMessageParams(this, getCallingApp(), mPlayer);
- mAudioPlaybackHandler.enqueueAudio(mToken);
+ mAudioPlaybackHandler.enqueue(mItem);
return TextToSpeech.SUCCESS;
}
@@ -707,10 +721,10 @@ public abstract class TextToSpeechService extends Service {
private class SilenceSpeechItem extends SpeechItem {
private final long mDuration;
- private SilenceMessageParams mToken;
- public SilenceSpeechItem(String callingApp, Bundle params, long duration) {
- super(callingApp, params);
+ public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
+ Bundle params, long duration) {
+ super(callerIdentity, callerUid, callerPid, params);
mDuration = duration;
}
@@ -721,14 +735,14 @@ public abstract class TextToSpeechService extends Service {
@Override
protected int playImpl() {
- mToken = new SilenceMessageParams(this, getCallingApp(), mDuration);
- mAudioPlaybackHandler.enqueueSilence(mToken);
+ mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
+ this, getCallerIdentity(), mDuration));
return TextToSpeech.SUCCESS;
}
@Override
protected void stopImpl() {
- // Do nothing.
+ // Do nothing, handled by AudioPlaybackHandler#stopForApp
}
}
@@ -747,58 +761,67 @@ public abstract class TextToSpeechService extends Service {
// NOTE: All calls that are passed in a calling app are interned so that
// they can be used as message objects (which are tested for equality using ==).
private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
-
- public int speak(String callingApp, String text, int queueMode, Bundle params) {
- if (!checkNonNull(callingApp, text, params)) {
+ @Override
+ public int speak(IBinder caller, String text, int queueMode, Bundle params) {
+ if (!checkNonNull(caller, text, params)) {
return TextToSpeech.ERROR;
}
- SpeechItem item = new SynthesisSpeechItem(intern(callingApp), params, text);
+ SpeechItem item = new SynthesisSpeechItem(caller,
+ Binder.getCallingUid(), Binder.getCallingPid(), params, text);
return mSynthHandler.enqueueSpeechItem(queueMode, item);
}
- public int synthesizeToFile(String callingApp, String text, String filename,
+ @Override
+ public int synthesizeToFile(IBinder caller, String text, String filename,
Bundle params) {
- if (!checkNonNull(callingApp, text, filename, params)) {
+ if (!checkNonNull(caller, text, filename, params)) {
return TextToSpeech.ERROR;
}
File file = new File(filename);
- SpeechItem item = new SynthesisToFileSpeechItem(intern(callingApp),
- params, text, file);
+ SpeechItem item = new SynthesisToFileSpeechItem(caller, Binder.getCallingUid(),
+ Binder.getCallingPid(), params, text, file);
return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
}
- public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) {
- if (!checkNonNull(callingApp, audioUri, params)) {
+ @Override
+ public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) {
+ if (!checkNonNull(caller, audioUri, params)) {
return TextToSpeech.ERROR;
}
- SpeechItem item = new AudioSpeechItem(intern(callingApp), params, audioUri);
+ SpeechItem item = new AudioSpeechItem(caller,
+ Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri);
return mSynthHandler.enqueueSpeechItem(queueMode, item);
}
- public int playSilence(String callingApp, long duration, int queueMode, Bundle params) {
- if (!checkNonNull(callingApp, params)) {
+ @Override
+ public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) {
+ if (!checkNonNull(caller, params)) {
return TextToSpeech.ERROR;
}
- SpeechItem item = new SilenceSpeechItem(intern(callingApp), params, duration);
+ SpeechItem item = new SilenceSpeechItem(caller,
+ Binder.getCallingUid(), Binder.getCallingPid(), params, duration);
return mSynthHandler.enqueueSpeechItem(queueMode, item);
}
+ @Override
public boolean isSpeaking() {
return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
}
- public int stop(String callingApp) {
- if (!checkNonNull(callingApp)) {
+ @Override
+ public int stop(IBinder caller) {
+ if (!checkNonNull(caller)) {
return TextToSpeech.ERROR;
}
- return mSynthHandler.stopForApp(intern(callingApp));
+ return mSynthHandler.stopForApp(caller);
}
+ @Override
public String[] getLanguage() {
return onGetLanguage();
}
@@ -807,6 +830,7 @@ public abstract class TextToSpeechService extends Service {
* If defaults are enforced, then no language is "available" except
* perhaps the default language selected by the user.
*/
+ @Override
public int isLanguageAvailable(String lang, String country, String variant) {
if (!checkNonNull(lang)) {
return TextToSpeech.ERROR;
@@ -815,6 +839,7 @@ public abstract class TextToSpeechService extends Service {
return onIsLanguageAvailable(lang, country, variant);
}
+ @Override
public String[] getFeaturesForLanguage(String lang, String country, String variant) {
Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
String[] featuresArray = null;
@@ -831,6 +856,7 @@ public abstract class TextToSpeechService extends Service {
* There is no point loading a non default language if defaults
* are enforced.
*/
+ @Override
public int loadLanguage(String lang, String country, String variant) {
if (!checkNonNull(lang)) {
return TextToSpeech.ERROR;
@@ -839,13 +865,14 @@ public abstract class TextToSpeechService extends Service {
return onLoadLanguage(lang, country, variant);
}
- public void setCallback(String packageName, ITextToSpeechCallback cb) {
+ @Override
+ public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
// Note that passing in a null callback is a valid use case.
- if (!checkNonNull(packageName)) {
+ if (!checkNonNull(caller)) {
return;
}
- mCallbacks.setCallback(packageName, cb);
+ mCallbacks.setCallback(caller, cb);
}
private String intern(String in) {
@@ -862,18 +889,17 @@ public abstract class TextToSpeechService extends Service {
};
private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
+ private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
+ = new HashMap<IBinder, ITextToSpeechCallback>();
- private final HashMap<String, ITextToSpeechCallback> mAppToCallback
- = new HashMap<String, ITextToSpeechCallback>();
-
- public void setCallback(String packageName, ITextToSpeechCallback cb) {
- synchronized (mAppToCallback) {
+ public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
+ synchronized (mCallerToCallback) {
ITextToSpeechCallback old;
if (cb != null) {
- register(cb, packageName);
- old = mAppToCallback.put(packageName, cb);
+ register(cb, caller);
+ old = mCallerToCallback.put(caller, cb);
} else {
- old = mAppToCallback.remove(packageName);
+ old = mCallerToCallback.remove(caller);
}
if (old != null && old != cb) {
unregister(old);
@@ -881,8 +907,8 @@ public abstract class TextToSpeechService extends Service {
}
}
- public void dispatchOnDone(String packageName, String utteranceId) {
- ITextToSpeechCallback cb = getCallbackFor(packageName);
+ public void dispatchOnDone(Object callerIdentity, String utteranceId) {
+ ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
if (cb == null) return;
try {
cb.onDone(utteranceId);
@@ -891,8 +917,8 @@ public abstract class TextToSpeechService extends Service {
}
}
- public void dispatchOnStart(String packageName, String utteranceId) {
- ITextToSpeechCallback cb = getCallbackFor(packageName);
+ public void dispatchOnStart(Object callerIdentity, String utteranceId) {
+ ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
if (cb == null) return;
try {
cb.onStart(utteranceId);
@@ -902,8 +928,8 @@ public abstract class TextToSpeechService extends Service {
}
- public void dispatchOnError(String packageName, String utteranceId) {
- ITextToSpeechCallback cb = getCallbackFor(packageName);
+ public void dispatchOnError(Object callerIdentity, String utteranceId) {
+ ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
if (cb == null) return;
try {
cb.onError(utteranceId);
@@ -914,25 +940,26 @@ public abstract class TextToSpeechService extends Service {
@Override
public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
- String packageName = (String) cookie;
- synchronized (mAppToCallback) {
- mAppToCallback.remove(packageName);
+ IBinder caller = (IBinder) cookie;
+ synchronized (mCallerToCallback) {
+ mCallerToCallback.remove(caller);
}
- mSynthHandler.stopForApp(packageName);
+ mSynthHandler.stopForApp(caller);
}
@Override
public void kill() {
- synchronized (mAppToCallback) {
- mAppToCallback.clear();
+ synchronized (mCallerToCallback) {
+ mCallerToCallback.clear();
super.kill();
}
}
- private ITextToSpeechCallback getCallbackFor(String packageName) {
+ private ITextToSpeechCallback getCallbackFor(Object caller) {
ITextToSpeechCallback cb;
- synchronized (mAppToCallback) {
- cb = mAppToCallback.get(packageName);
+ IBinder asBinder = (IBinder) caller;
+ synchronized (mCallerToCallback) {
+ cb = mCallerToCallback.get(asBinder);
}
return cb;
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
index c184c11..a52e2ba 100644
--- a/core/java/android/text/MeasuredText.java
+++ b/core/java/android/text/MeasuredText.java
@@ -109,6 +109,9 @@ class MeasuredText {
for (int i = 0; i < spans.length; i++) {
int startInPara = spanned.getSpanStart(spans[i]) - start;
int endInPara = spanned.getSpanEnd(spans[i]) - start;
+ // The span interval may be larger and must be restricted to [start, end[
+ if (startInPara < 0) startInPara = 0;
+ if (endInPara > len) endInPara = len;
for (int j = startInPara; j < endInPara; j++) {
mChars[j] = '\uFFFC';
}
diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java
index 7f8af7a..da10311 100644
--- a/core/java/android/text/format/DateUtils.java
+++ b/core/java/android/text/format/DateUtils.java
@@ -136,12 +136,12 @@ public class DateUtils
private static java.text.DateFormat sStatusTimeFormat;
private static String sElapsedFormatMMSS;
private static String sElapsedFormatHMMSS;
-
+
private static final String FAST_FORMAT_HMMSS = "%1$d:%2$02d:%3$02d";
private static final String FAST_FORMAT_MMSS = "%1$02d:%2$02d";
private static final char TIME_PADDING = '0';
private static final char TIME_SEPARATOR = ':';
-
+
public static final long SECOND_IN_MILLIS = 1000;
public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
@@ -201,7 +201,7 @@ public class DateUtils
public static final String YEAR_FORMAT_TWO_DIGITS = "%g";
public static final String WEEKDAY_FORMAT = "%A";
public static final String ABBREV_WEEKDAY_FORMAT = "%a";
-
+
// This table is used to lookup the resource string id of a format string
// used for formatting a start and end date that fall in the same year.
// The index is constructed from a bit-wise OR of the boolean values:
@@ -227,7 +227,7 @@ public class DateUtils
com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2,
com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2,
};
-
+
// This table is used to lookup the resource string id of a format string
// used for formatting a start and end date that fall in the same month.
// The index is constructed from a bit-wise OR of the boolean values:
@@ -256,7 +256,7 @@ public class DateUtils
/**
* Request the full spelled-out name. For use with the 'abbrev' parameter of
* {@link #getDayOfWeekString} and {@link #getMonthString}.
- *
+ *
* @more <p>
* e.g. "Sunday" or "January"
*/
@@ -265,7 +265,7 @@ public class DateUtils
/**
* Request an abbreviated version of the name. For use with the 'abbrev'
* parameter of {@link #getDayOfWeekString} and {@link #getMonthString}.
- *
+ *
* @more <p>
* e.g. "Sun" or "Jan"
*/
@@ -347,7 +347,7 @@ public class DateUtils
* @return Localized month of the year.
*/
public static String getMonthString(int month, int abbrev) {
- // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
+ // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
// This is a shortcut to not spam the translators with too many variations
// of the same string. If we find that in a language the distinction
// is necessary, we can can add more without changing this API.
@@ -380,7 +380,7 @@ public class DateUtils
* @hide Pending API council approval
*/
public static String getStandaloneMonthString(int month, int abbrev) {
- // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
+ // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
// This is a shortcut to not spam the translators with too many variations
// of the same string. If we find that in a language the distinction
// is necessary, we can can add more without changing this API.
@@ -434,7 +434,7 @@ public class DateUtils
* <p>
* Can use {@link #FORMAT_ABBREV_RELATIVE} flag to use abbreviated relative
* times, like "42 mins ago".
- *
+ *
* @param time the time to describe, in milliseconds
* @param now the current time in milliseconds
* @param minResolution the minimum timespan to report. For example, a time
@@ -450,7 +450,7 @@ public class DateUtils
int flags) {
Resources r = Resources.getSystem();
boolean abbrevRelative = (flags & (FORMAT_ABBREV_RELATIVE | FORMAT_ABBREV_ALL)) != 0;
-
+
boolean past = (now >= time);
long duration = Math.abs(now - time);
@@ -525,7 +525,7 @@ public class DateUtils
String format = r.getQuantityString(resId, (int) count);
return String.format(format, count);
}
-
+
/**
* Returns the number of days passed between two dates.
*
@@ -555,7 +555,7 @@ public class DateUtils
* <li>Dec 12, 4:12 AM</li>
* <li>11/14/2007, 8:20 AM</li>
* </ul>
- *
+ *
* @param time some time in the past.
* @param minResolution the minimum elapsed time (in milliseconds) to report
* when showing relative times. For example, a time 3 seconds in
@@ -584,7 +584,7 @@ public class DateUtils
}
CharSequence timeClause = formatDateRange(c, time, time, FORMAT_SHOW_TIME);
-
+
String result;
if (duration < transitionResolution) {
CharSequence relativeClause = getRelativeTimeSpanString(time, now, minResolution, flags);
@@ -601,7 +601,7 @@ public class DateUtils
* Returns a string describing a day relative to the current day. For example if the day is
* today this function returns "Today", if the day was a week ago it returns "7 days ago", and
* if the day is in 2 weeks it returns "in 14 days".
- *
+ *
* @param r the resources to get the strings from
* @param day the relative day to describe in UTC milliseconds
* @param today the current time in UTC milliseconds
@@ -618,7 +618,7 @@ public class DateUtils
int days = Math.abs(currentDay - startDay);
boolean past = (today > day);
-
+
if (days == 1) {
if (past) {
return r.getString(com.android.internal.R.string.yesterday);
@@ -635,7 +635,7 @@ public class DateUtils
} else {
resId = com.android.internal.R.plurals.in_num_days;
}
-
+
String format = r.getQuantityString(resId, days);
return String.format(format, days);
}
@@ -677,11 +677,11 @@ public class DateUtils
public static String formatElapsedTime(long elapsedSeconds) {
return formatElapsedTime(null, elapsedSeconds);
}
-
+
/**
* Formats an elapsed time in the form "MM:SS" or "H:MM:SS"
* for display on the call-in-progress screen.
- *
+ *
* @param recycle {@link StringBuilder} to recycle, if possible
* @param elapsedSeconds the elapsed time in seconds.
*/
@@ -724,7 +724,7 @@ public class DateUtils
}
sb.append(hours);
sb.append(TIME_SEPARATOR);
- if (minutes < 10) {
+ if (minutes < 10) {
sb.append(TIME_PADDING);
} else {
sb.append(toDigitChar(minutes / 10));
@@ -755,7 +755,7 @@ public class DateUtils
} else {
sb.setLength(0);
}
- if (minutes < 10) {
+ if (minutes < 10) {
sb.append(TIME_PADDING);
} else {
sb.append(toDigitChar(minutes / 10));
@@ -777,11 +777,11 @@ public class DateUtils
private static char toDigitChar(long digit) {
return (char) (digit + '0');
}
-
+
/**
* Format a date / time such that if the then is on the same day as now, it shows
* just the time and if it's a different day, it shows just the date.
- *
+ *
* <p>The parameters dateFormat and timeFormat should each be one of
* {@link java.text.DateFormat#DEFAULT},
* {@link java.text.DateFormat#FULL},
@@ -833,14 +833,14 @@ public class DateUtils
public static boolean isToday(long when) {
Time time = new Time();
time.set(when);
-
+
int thenYear = time.year;
int thenMonth = time.month;
int thenMonthDay = time.monthDay;
time.set(System.currentTimeMillis());
return (thenYear == time.year)
- && (thenMonth == time.month)
+ && (thenMonth == time.month)
&& (thenMonthDay == time.monthDay);
}
@@ -914,7 +914,7 @@ public class DateUtils
public static String writeDateTime(Calendar cal, StringBuilder sb)
{
int n;
-
+
n = cal.get(Calendar.YEAR);
sb.setCharAt(3, (char)('0'+n%10));
n /= 10;
@@ -1015,7 +1015,7 @@ public class DateUtils
/**
* Formats a date or a time range according to the local conventions.
- *
+ *
* <p>
* Example output strings (date formats in these examples are shown using
* the US date format convention but that may change depending on the
@@ -1036,10 +1036,10 @@ public class DateUtils
* <li>Oct 9, 8:00am - Oct 10, 5:00pm</li>
* <li>12/31/2007 - 01/01/2008</li>
* </ul>
- *
+ *
* <p>
* The flags argument is a bitmask of options from the following list:
- *
+ *
* <ul>
* <li>FORMAT_SHOW_TIME</li>
* <li>FORMAT_SHOW_WEEKDAY</li>
@@ -1061,15 +1061,15 @@ public class DateUtils
* <li>FORMAT_ABBREV_ALL</li>
* <li>FORMAT_NUMERIC_DATE</li>
* </ul>
- *
+ *
* <p>
* If FORMAT_SHOW_TIME is set, the time is shown as part of the date range.
* If the start and end time are the same, then just the start time is
* shown.
- *
+ *
* <p>
* If FORMAT_SHOW_WEEKDAY is set, then the weekday is shown.
- *
+ *
* <p>
* If FORMAT_SHOW_YEAR is set, then the year is always shown.
* If FORMAT_NO_YEAR is set, then the year is not shown.
@@ -1082,80 +1082,91 @@ public class DateUtils
* Normally the date is shown unless the start and end day are the same.
* If FORMAT_SHOW_DATE is set, then the date is always shown, even for
* same day ranges.
- *
+ *
* <p>
* If FORMAT_NO_MONTH_DAY is set, then if the date is shown, just the
* month name will be shown, not the day of the month. For example,
* "January, 2008" instead of "January 6 - 12, 2008".
- *
+ *
* <p>
* If FORMAT_CAP_AMPM is set and 12-hour time is used, then the "AM"
* and "PM" are capitalized. You should not use this flag
* because in some locales these terms cannot be capitalized, and in
* many others it doesn't make sense to do so even though it is possible.
- *
+ *
* <p>
* If FORMAT_NO_NOON is set and 12-hour time is used, then "12pm" is
* shown instead of "noon".
- *
+ *
* <p>
* If FORMAT_CAP_NOON is set and 12-hour time is used, then "Noon" is
* shown instead of "noon". You should probably not use this flag
* because in many locales it will not make sense to capitalize
* the term.
- *
+ *
* <p>
* If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is
* shown instead of "midnight".
- *
+ *
* <p>
* If FORMAT_CAP_MIDNIGHT is set and 12-hour time is used, then "Midnight"
* is shown instead of "midnight". You should probably not use this
* flag because in many locales it will not make sense to capitalize
* the term.
- *
+ *
* <p>
* If FORMAT_12HOUR is set and the time is shown, then the time is
* shown in the 12-hour time format. You should not normally set this.
* Instead, let the time format be chosen automatically according to the
* system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then
* FORMAT_24HOUR takes precedence.
- *
+ *
* <p>
* If FORMAT_24HOUR is set and the time is shown, then the time is
* shown in the 24-hour time format. You should not normally set this.
* Instead, let the time format be chosen automatically according to the
* system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then
* FORMAT_24HOUR takes precedence.
- *
+ *
* <p>
* If FORMAT_UTC is set, then the UTC time zone is used for the start
* and end milliseconds unless a time zone is specified. If a time zone
* is specified it will be used regardless of the FORMAT_UTC flag.
- *
+ *
* <p>
* If FORMAT_ABBREV_TIME is set and 12-hour time format is used, then the
* start and end times (if shown) are abbreviated by not showing the minutes
* if they are zero. For example, instead of "3:00pm" the time would be
* abbreviated to "3pm".
- *
+ *
* <p>
* If FORMAT_ABBREV_WEEKDAY is set, then the weekday (if shown) is
* abbreviated to a 3-letter string.
- *
+ *
* <p>
* If FORMAT_ABBREV_MONTH is set, then the month (if shown) is abbreviated
* to a 3-letter string.
- *
+ *
* <p>
* If FORMAT_ABBREV_ALL is set, then the weekday and the month (if shown)
* are abbreviated to 3-letter strings.
- *
+ *
* <p>
* If FORMAT_NUMERIC_DATE is set, then the date is shown in numeric format
* instead of using the name of the month. For example, "12/31/2008"
* instead of "December 31, 2008".
- *
+ *
+ * <p>
+ * If the end date ends at 12:00am at the beginning of a day, it is
+ * formatted as the end of the previous day in two scenarios:
+ * <ul>
+ * <li>For single day events. This results in "8pm - midnight" instead of
+ * "Nov 10, 8pm - Nov 11, 12am".</li>
+ * <li>When the time is not displayed. This results in "Nov 10 - 11" for
+ * an event with a start date of Nov 10 and an end date of Nov 12 at
+ * 00:00.</li>
+ * </ul>
+ *
* @param context the context is required only if the time is shown
* @param formatter the Formatter used for formatting the date range.
* Note: be sure to call setLength(0) on StringBuilder passed to
@@ -1165,7 +1176,7 @@ public class DateUtils
* @param flags a bit mask of options
* @param timeZone the time zone to compute the string in. Use null for local
* or if the FORMAT_UTC flag is being used.
- *
+ *
* @return the formatter with the formatted date/time range appended to the string buffer.
*/
public static Formatter formatDateRange(Context context, Formatter formatter, long startMillis,
@@ -1215,20 +1226,6 @@ public class DateUtils
dayDistance = endJulianDay - startJulianDay;
}
- // If the end date ends at 12am at the beginning of a day,
- // then modify it to make it look like it ends at midnight on
- // the previous day. This will allow us to display "8pm - midnight",
- // for example, instead of "Nov 10, 8pm - Nov 11, 12am". But we only do
- // this if it is midnight of the same day as the start date because
- // for multiple-day events, an end time of "midnight on Nov 11" is
- // ambiguous and confusing (is that midnight the start of Nov 11, or
- // the end of Nov 11?).
- // If we are not showing the time then also adjust the end date
- // for multiple-day events. This is to allow us to display, for
- // example, "Nov 10 -11" for an event with a start date of Nov 10
- // and an end date of Nov 12 at 00:00.
- // If the start and end time are the same, then skip this and don't
- // adjust the date.
if (!isInstant
&& (endDate.hour | endDate.minute | endDate.second) == 0
&& (!showTime || dayDistance <= 1)) {
@@ -1592,7 +1589,7 @@ public class DateUtils
* <li>Wed, October 31</li>
* <li>10/31/2007</li>
* </ul>
- *
+ *
* @param context the context is required only if the time is shown
* @param millis a point in time in UTC milliseconds
* @param flags a bit mask of formatting options
@@ -1607,13 +1604,13 @@ public class DateUtils
* are counted starting at midnight, which means that assuming that the current
* time is March 31st, 0:30:
* <ul>
- * <li>"millis=0:10 today" will be displayed as "0:10"</li>
+ * <li>"millis=0:10 today" will be displayed as "0:10"</li>
* <li>"millis=11:30pm the day before" will be displayed as "Mar 30"</li>
* </ul>
* If the given millis is in a different year, then the full date is
* returned in numeric format (e.g., "10/12/2008").
- *
- * @param withPreposition If true, the string returned will include the correct
+ *
+ * @param withPreposition If true, the string returned will include the correct
* preposition ("at 9:20am", "on 10/12/2008" or "on May 29").
*/
public static CharSequence getRelativeTimeSpanString(Context c, long millis,
@@ -1661,9 +1658,9 @@ public class DateUtils
}
return result;
}
-
+
/**
- * Convenience function to return relative time string without preposition.
+ * Convenience function to return relative time string without preposition.
* @param c context for resources
* @param millis time in milliseconds
* @return {@link CharSequence} containing relative time.
@@ -1672,7 +1669,7 @@ public class DateUtils
public static CharSequence getRelativeTimeSpanString(Context c, long millis) {
return getRelativeTimeSpanString(c, millis, false /* no preposition */);
}
-
+
private static Time sNowTime;
private static Time sThenTime;
}
diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java
index b4445ca..e9b0d32 100644
--- a/core/java/android/text/format/Time.java
+++ b/core/java/android/text/format/Time.java
@@ -481,6 +481,9 @@ public class Time {
* @throws android.util.TimeFormatException if s cannot be parsed.
*/
public boolean parse3339(String s) {
+ if (s == null) {
+ throw new NullPointerException("time string is null");
+ }
if (nativeParse3339(s)) {
timezone = TIMEZONE_UTC;
return true;
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index 4ec4bc4..30bb447 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -280,8 +280,6 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
if (isSelecting(buffer)) {
buffer.removeSpan(LAST_TAP_DOWN);
Selection.extendSelection(buffer, offset);
- } else if (!widget.shouldIgnoreActionUpEvent()) {
- Selection.setSelection(buffer, offset);
}
MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java
index 0f26a34..5dc206f 100644
--- a/core/java/android/text/style/SuggestionSpan.java
+++ b/core/java/android/text/style/SuggestionSpan.java
@@ -25,6 +25,7 @@ import android.os.SystemClock;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+import android.util.Log;
import android.widget.TextView;
import java.util.Arrays;
@@ -114,7 +115,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
* @param context Context for the application
* @param locale locale Locale of the suggestions
* @param suggestions Suggestions for the string under the span. Only the first up to
- * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered.
+ * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. Null values not permitted.
* @param flags Additional flags indicating how this span is handled in TextView
* @param notificationTargetClass if not null, this class will get notified when the user
* selects one of the suggestions.
@@ -124,10 +125,13 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length);
mSuggestions = Arrays.copyOf(suggestions, N);
mFlags = flags;
- if (context != null && locale == null) {
+ if (locale != null) {
+ mLocaleString = locale.toString();
+ } else if (context != null) {
mLocaleString = context.getResources().getConfiguration().locale.toString();
} else {
- mLocaleString = locale.toString();
+ Log.e("SuggestionSpan", "No locale or context specified in SuggestionSpan constructor");
+ mLocaleString = "";
}
if (notificationTargetClass != null) {
diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java
index 519b980..a43d36c 100644
--- a/core/java/android/util/DisplayMetrics.java
+++ b/core/java/android/util/DisplayMetrics.java
@@ -57,6 +57,13 @@ public class DisplayMetrics {
public static final int DENSITY_XHIGH = 320;
/**
+ * Standard quantized DPI for extra-extra-high-density screens. Applications
+ * should not generally worry about this density; relying on XHIGH graphics
+ * being scaled up to it should be sufficient for almost all cases.
+ */
+ public static final int DENSITY_XXHIGH = 480;
+
+ /**
* The reference density used throughout the system.
*/
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java
new file mode 100644
index 0000000..641d1b4
--- /dev/null
+++ b/core/java/android/util/LocalLog.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.text.format.Time;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * @hide
+ */
+public final class LocalLog {
+
+ private LinkedList<String> mLog;
+ private int mMaxLines;
+ private Time mNow;
+
+ public LocalLog(int maxLines) {
+ mLog = new LinkedList<String>();
+ mMaxLines = maxLines;
+ mNow = new Time();
+ }
+
+ public synchronized void log(String msg) {
+ if (mMaxLines > 0) {
+ mNow.setToNow();
+ mLog.add(mNow.format("%H:%M:%S") + " - " + msg);
+ while (mLog.size() > mMaxLines) mLog.remove();
+ }
+ }
+
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ Iterator<String> itr = mLog.listIterator(0);
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+}
diff --git a/core/java/android/util/LocaleUtil.java b/core/java/android/util/LocaleUtil.java
index 763af73..4d773f6 100644
--- a/core/java/android/util/LocaleUtil.java
+++ b/core/java/android/util/LocaleUtil.java
@@ -39,8 +39,6 @@ public class LocaleUtil {
*/
public static final int TEXT_LAYOUT_DIRECTION_RTL_DO_NOT_USE = 1;
- private static final char UNDERSCORE_CHAR = '_';
-
private static String ARAB_SCRIPT_SUBTAG = "Arab";
private static String HEBR_SCRIPT_SUBTAG = "Hebr";
diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java
index 5540000..51e373c 100644
--- a/core/java/android/util/LruCache.java
+++ b/core/java/android/util/LruCache.java
@@ -54,6 +54,10 @@ import java.util.Map;
* <p>This class does not allow null to be used as a key or value. A return
* value of null from {@link #get}, {@link #put} or {@link #remove} is
* unambiguous: the key was not in the cache.
+ *
+ * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
+ * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
+ * Support Package</a> for earlier releases.
*/
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
@@ -82,6 +86,23 @@ public class LruCache<K, V> {
}
/**
+ * Sets the size of the cache.
+ * @param maxSize The new maximum size.
+ *
+ * @hide
+ */
+ public void resize(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+
+ synchronized (this) {
+ this.maxSize = maxSize;
+ }
+ trimToSize(maxSize);
+ }
+
+ /**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java
new file mode 100644
index 0000000..a08d5cb
--- /dev/null
+++ b/core/java/android/util/SparseLongArray.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * SparseLongArrays map integers to longs. Unlike a normal array of longs,
+ * there can be gaps in the indices. It is intended to be more efficient
+ * than using a HashMap to map Integers to Longs.
+ *
+ * @hide
+ */
+public class SparseLongArray implements Cloneable {
+
+ private int[] mKeys;
+ private long[] mValues;
+ private int mSize;
+
+ /**
+ * Creates a new SparseLongArray containing no mappings.
+ */
+ public SparseLongArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseLongArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings.
+ */
+ public SparseLongArray(int initialCapacity) {
+ initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity);
+
+ mKeys = new int[initialCapacity];
+ mValues = new long[initialCapacity];
+ mSize = 0;
+ }
+
+ @Override
+ public SparseLongArray clone() {
+ SparseLongArray clone = null;
+ try {
+ clone = (SparseLongArray) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public long get(int key) {
+ return get(key, 0);
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public long get(int key, long valueIfKeyNotFound) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i < 0) {
+ return valueIfKeyNotFound;
+ } else {
+ return mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i >= 0) {
+ removeAt(i);
+ }
+ }
+
+ /**
+ * Removes the mapping at the given index.
+ */
+ public void removeAt(int index) {
+ System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+ mSize--;
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, long value) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (mSize >= mKeys.length) {
+ growKeyAndValueArrays(mSize + 1);
+ }
+
+ if (mSize - i != 0) {
+ System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+ System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+ }
+
+ mKeys[i] = key;
+ mValues[i] = value;
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseIntArray
+ * currently stores.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ */
+ public int keyAt(int index) {
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ */
+ public long valueAt(int index) {
+ return mValues[index];
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return binarySearch(mKeys, 0, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(long value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseIntArray.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, long value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ int pos = mSize;
+ if (pos >= mKeys.length) {
+ growKeyAndValueArrays(pos + 1);
+ }
+
+ mKeys[pos] = key;
+ mValues[pos] = value;
+ mSize = pos + 1;
+ }
+
+ private void growKeyAndValueArrays(int minNeededSize) {
+ int n = ArrayUtils.idealLongArraySize(minNeededSize);
+
+ int[] nkeys = new int[n];
+ long[] nvalues = new long[n];
+
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ private static int binarySearch(int[] a, int start, int len, long key) {
+ int high = start + len, low = start - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (a[guess] < key)
+ low = guess;
+ else
+ high = guess;
+ }
+
+ if (high == start + len)
+ return ~(start + len);
+ else if (a[high] == key)
+ return high;
+ else
+ return ~high;
+ }
+}
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
new file mode 100644
index 0000000..63de128
--- /dev/null
+++ b/core/java/android/view/Choreographer.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.internal.util.ArrayUtils;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+
+/**
+ * Coodinates animations and drawing for UI on a particular thread.
+ * @hide
+ */
+public final class Choreographer extends Handler {
+ private static final String TAG = "Choreographer";
+ private static final boolean DEBUG = false;
+
+ // The default amount of time in ms between animation frames.
+ // When vsync is not enabled, we want to have some idea of how long we should
+ // wait before posting the next animation message. It is important that the
+ // default value be less than the true inter-frame delay on all devices to avoid
+ // situations where we might skip frames by waiting too long (we must compensate
+ // for jitter and hardware variations). Regardless of this value, the animation
+ // and display loop is ultimately rate-limited by how fast new graphics buffers can
+ // be dequeued.
+ private static final long DEFAULT_FRAME_DELAY = 10;
+
+ // The number of milliseconds between animation frames.
+ private static long sFrameDelay = DEFAULT_FRAME_DELAY;
+
+ // Thread local storage for the choreographer.
+ private static final ThreadLocal<Choreographer> sThreadInstance =
+ new ThreadLocal<Choreographer>() {
+ @Override
+ protected Choreographer initialValue() {
+ Looper looper = Looper.myLooper();
+ if (looper == null) {
+ throw new IllegalStateException("The current thread must have a looper!");
+ }
+ return new Choreographer(looper);
+ }
+ };
+
+ // System property to enable/disable vsync for animations and drawing.
+ // Enabled by default.
+ private static final boolean USE_VSYNC = SystemProperties.getBoolean(
+ "debug.choreographer.vsync", true);
+
+ // System property to enable/disable the use of the vsync / animation timer
+ // for drawing rather than drawing immediately.
+ // Temporarily disabled by default because postponing performTraversals() violates
+ // assumptions about traversals happening in-order relative to other posted messages.
+ // Bug: 5721047
+ private static final boolean USE_ANIMATION_TIMER_FOR_DRAW = SystemProperties.getBoolean(
+ "debug.choreographer.animdraw", false);
+
+ private static final int MSG_DO_ANIMATION = 0;
+ private static final int MSG_DO_DRAW = 1;
+
+ private final Looper mLooper;
+
+ private OnAnimateListener[] mOnAnimateListeners;
+ private OnDrawListener[] mOnDrawListeners;
+
+ private boolean mAnimationScheduled;
+ private boolean mDrawScheduled;
+ private FrameDisplayEventReceiver mFrameDisplayEventReceiver;
+ private long mLastAnimationTime;
+ private long mLastDrawTime;
+
+ private Choreographer(Looper looper) {
+ super(looper);
+ mLooper = looper;
+ mLastAnimationTime = Long.MIN_VALUE;
+ mLastDrawTime = Long.MIN_VALUE;
+ }
+
+ /**
+ * Gets the choreographer for this thread.
+ * Must be called on the UI thread.
+ *
+ * @return The choreographer for this thread.
+ * @throws IllegalStateException if the thread does not have a looper.
+ */
+ public static Choreographer getInstance() {
+ return sThreadInstance.get();
+ }
+
+ /**
+ * 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.
+ *
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ *
+ * @return the requested time between frames, in milliseconds
+ */
+ public static long getFrameDelay() {
+ return sFrameDelay;
+ }
+
+ /**
+ * 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.
+ *
+ * The frame delay may be ignored when the animation system uses an external timing
+ * source, such as the display refresh rate (vsync), to govern animations.
+ *
+ * @param frameDelay the requested time between frames, in milliseconds
+ */
+ public static void setFrameDelay(long frameDelay) {
+ sFrameDelay = frameDelay;
+ }
+
+ /**
+ * Schedules animation (and drawing) to occur on the next frame synchronization boundary.
+ * Must be called on the UI thread.
+ */
+ public void scheduleAnimation() {
+ if (!mAnimationScheduled) {
+ mAnimationScheduled = true;
+ if (USE_VSYNC) {
+ if (DEBUG) {
+ Log.d(TAG, "Scheduling vsync for animation.");
+ }
+ if (mFrameDisplayEventReceiver == null) {
+ mFrameDisplayEventReceiver = new FrameDisplayEventReceiver(mLooper);
+ }
+ mFrameDisplayEventReceiver.scheduleVsync();
+ } else {
+ final long now = SystemClock.uptimeMillis();
+ final long nextAnimationTime = Math.max(mLastAnimationTime + sFrameDelay, now);
+ if (DEBUG) {
+ Log.d(TAG, "Scheduling animation in " + (nextAnimationTime - now) + " ms.");
+ }
+ sendEmptyMessageAtTime(MSG_DO_ANIMATION, nextAnimationTime);
+ }
+ }
+ }
+
+ /**
+ * Schedules drawing to occur on the next frame synchronization boundary.
+ * Must be called on the UI thread.
+ */
+ public void scheduleDraw() {
+ if (!mDrawScheduled) {
+ mDrawScheduled = true;
+ if (USE_ANIMATION_TIMER_FOR_DRAW) {
+ scheduleAnimation();
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Scheduling draw immediately.");
+ }
+ sendEmptyMessage(MSG_DO_DRAW);
+ }
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DO_ANIMATION:
+ doAnimation();
+ break;
+ case MSG_DO_DRAW:
+ doDraw();
+ break;
+ }
+ }
+
+ private void doAnimation() {
+ if (mAnimationScheduled) {
+ mAnimationScheduled = false;
+
+ final long start = SystemClock.uptimeMillis();
+ if (DEBUG) {
+ Log.d(TAG, "Performing animation: " + Math.max(0, start - mLastAnimationTime)
+ + " ms have elapsed since previous animation.");
+ }
+ mLastAnimationTime = start;
+
+ final OnAnimateListener[] listeners = mOnAnimateListeners;
+ if (listeners != null) {
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].onAnimate();
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Animation took " + (SystemClock.uptimeMillis() - start) + " ms.");
+ }
+ }
+
+ if (USE_ANIMATION_TIMER_FOR_DRAW) {
+ doDraw();
+ }
+ }
+
+ private void doDraw() {
+ if (mDrawScheduled) {
+ mDrawScheduled = false;
+
+ final long start = SystemClock.uptimeMillis();
+ if (DEBUG) {
+ Log.d(TAG, "Performing draw: " + Math.max(0, start - mLastDrawTime)
+ + " ms have elapsed since previous draw.");
+ }
+ mLastDrawTime = start;
+
+ final OnDrawListener[] listeners = mOnDrawListeners;
+ if (listeners != null) {
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].onDraw();
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Draw took " + (SystemClock.uptimeMillis() - start) + " ms.");
+ }
+ }
+ }
+
+ /**
+ * Adds an animation listener.
+ * Must be called on the UI thread.
+ *
+ * @param listener The listener to add.
+ */
+ public void addOnAnimateListener(OnAnimateListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Adding onAnimate listener: " + listener);
+ }
+
+ mOnAnimateListeners = ArrayUtils.appendElement(OnAnimateListener.class,
+ mOnAnimateListeners, listener);
+ }
+
+ /**
+ * Removes an animation listener.
+ * Must be called on the UI thread.
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeOnAnimateListener(OnAnimateListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Removing onAnimate listener: " + listener);
+ }
+
+ mOnAnimateListeners = ArrayUtils.removeElement(OnAnimateListener.class,
+ mOnAnimateListeners, listener);
+ stopTimingLoopIfNoListeners();
+ }
+
+ /**
+ * Adds a draw listener.
+ * Must be called on the UI thread.
+ *
+ * @param listener The listener to add.
+ */
+ public void addOnDrawListener(OnDrawListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Adding onDraw listener: " + listener);
+ }
+
+ mOnDrawListeners = ArrayUtils.appendElement(OnDrawListener.class,
+ mOnDrawListeners, listener);
+ }
+
+ /**
+ * Removes a draw listener.
+ * Must be called on the UI thread.
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeOnDrawListener(OnDrawListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener must not be null");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Removing onDraw listener: " + listener);
+ }
+
+ mOnDrawListeners = ArrayUtils.removeElement(OnDrawListener.class,
+ mOnDrawListeners, listener);
+ stopTimingLoopIfNoListeners();
+ }
+
+ private void stopTimingLoopIfNoListeners() {
+ if (mOnDrawListeners == null && mOnAnimateListeners == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Stopping timing loop.");
+ }
+
+ if (mAnimationScheduled) {
+ mAnimationScheduled = false;
+ if (!USE_VSYNC) {
+ removeMessages(MSG_DO_ANIMATION);
+ }
+ }
+
+ if (mDrawScheduled) {
+ mDrawScheduled = false;
+ if (!USE_ANIMATION_TIMER_FOR_DRAW) {
+ removeMessages(MSG_DO_DRAW);
+ }
+ }
+
+ if (mFrameDisplayEventReceiver != null) {
+ mFrameDisplayEventReceiver.dispose();
+ mFrameDisplayEventReceiver = null;
+ }
+ }
+ }
+
+ /**
+ * Listens for animation frame timing events.
+ */
+ public static interface OnAnimateListener {
+ /**
+ * Called to animate properties before drawing the frame.
+ */
+ public void onAnimate();
+ }
+
+ /**
+ * Listens for draw frame timing events.
+ */
+ public static interface OnDrawListener {
+ /**
+ * Called to draw the frame.
+ */
+ public void onDraw();
+ }
+
+ private final class FrameDisplayEventReceiver extends DisplayEventReceiver {
+ public FrameDisplayEventReceiver(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void onVsync(long timestampNanos, int frame) {
+ doAnimation();
+ }
+ }
+}
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
new file mode 100644
index 0000000..d6711ee
--- /dev/null
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import dalvik.system.CloseGuard;
+
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+/**
+ * Provides a low-level mechanism for an application to receive display events
+ * such as vertical sync.
+ * @hide
+ */
+public abstract class DisplayEventReceiver {
+ private static final String TAG = "DisplayEventReceiver";
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private int mReceiverPtr;
+
+ // We keep a reference message queue object here so that it is not
+ // GC'd while the native peer of the receiver is using them.
+ private MessageQueue mMessageQueue;
+
+ private static native int nativeInit(DisplayEventReceiver receiver,
+ MessageQueue messageQueue);
+ private static native void nativeDispose(int receiverPtr);
+ private static native void nativeScheduleVsync(int receiverPtr);
+
+ /**
+ * Creates a display event receiver.
+ *
+ * @param looper The looper to use when invoking callbacks.
+ */
+ public DisplayEventReceiver(Looper looper) {
+ if (looper == null) {
+ throw new IllegalArgumentException("looper must not be null");
+ }
+
+ mMessageQueue = looper.getQueue();
+ mReceiverPtr = nativeInit(this, mMessageQueue);
+
+ mCloseGuard.open("dispose");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Disposes the receiver.
+ */
+ public void dispose() {
+ if (mCloseGuard != null) {
+ mCloseGuard.close();
+ }
+ if (mReceiverPtr != 0) {
+ nativeDispose(mReceiverPtr);
+ mReceiverPtr = 0;
+ }
+ mMessageQueue = null;
+ }
+
+ /**
+ * Called when a vertical sync pulse is received.
+ * The recipient should render a frame and then call {@link #scheduleVsync}
+ * to schedule the next vertical sync pulse.
+ *
+ * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()}
+ * timebase.
+ * @param frame The frame number. Increases by one for each vertical sync interval.
+ */
+ public void onVsync(long timestampNanos, int frame) {
+ }
+
+ /**
+ * Schedules a single vertical sync pulse to be delivered when the next
+ * display frame begins.
+ */
+ public void scheduleVsync() {
+ if (mReceiverPtr == 0) {
+ Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ + "receiver has already been disposed.");
+ } else {
+ nativeScheduleVsync(mReceiverPtr);
+ }
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ private void dispatchVsync(long timestampNanos, int frame) {
+ onVsync(timestampNanos, frame);
+ }
+}
diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java
index 8f4ece0..fec0d4b 100644
--- a/core/java/android/view/DisplayList.java
+++ b/core/java/android/view/DisplayList.java
@@ -32,20 +32,20 @@ public abstract class DisplayList {
*
* @return A canvas to record drawing operations.
*/
- abstract HardwareCanvas start();
+ public abstract HardwareCanvas start();
/**
* Ends the recording for this display list. A display list cannot be
* replayed if recording is not finished.
*/
- abstract void end();
+ public abstract void end();
/**
* Invalidates the display list, indicating that it should be repopulated
* with new drawing commands prior to being used again. Calling this method
* causes calls to {@link #isValid()} to return <code>false</code>.
*/
- abstract void invalidate();
+ public abstract void invalidate();
/**
* Returns whether the display list is currently usable. If this returns false,
@@ -53,12 +53,12 @@ public abstract class DisplayList {
*
* @return boolean true if the display list is able to be replayed, false otherwise.
*/
- abstract boolean isValid();
+ public abstract boolean isValid();
/**
* Return the amount of memory used by this display list.
*
* @return The size of this display list in bytes
*/
- abstract int getSize();
+ public abstract int getSize();
}
diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java
index 4ca299f..748ec0c 100644
--- a/core/java/android/view/GLES20Canvas.java
+++ b/core/java/android/view/GLES20Canvas.java
@@ -22,6 +22,7 @@ import android.graphics.ColorFilter;
import android.graphics.DrawFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.graphics.Picture;
import android.graphics.PorterDuff;
@@ -61,6 +62,7 @@ class GLES20Canvas extends HardwareCanvas {
private final float[] mLine = new float[4];
private final Rect mClipBounds = new Rect();
+ private final RectF mPathBounds = new RectF();
private DrawFilter mFilter;
@@ -154,6 +156,7 @@ class GLES20Canvas extends HardwareCanvas {
static native void nSetTextureLayerTransform(int layerId, int matrix);
static native void nDestroyLayer(int layerId);
static native void nDestroyLayerDeferred(int layerId);
+ static native void nFlushLayer(int layerId);
static native boolean nCopyLayer(int layerId, int bitmap);
///////////////////////////////////////////////////////////////////////////
@@ -186,7 +189,7 @@ class GLES20Canvas extends HardwareCanvas {
}
private static native int nGetMaximumTextureWidth();
- private static native int nGetMaximumTextureHeight();
+ private static native int nGetMaximumTextureHeight();
///////////////////////////////////////////////////////////////////////////
// Setup
@@ -246,7 +249,7 @@ class GLES20Canvas extends HardwareCanvas {
private static native void nDisableVsync();
@Override
- void onPreDraw(Rect dirty) {
+ public void onPreDraw(Rect dirty) {
if (dirty != null) {
nPrepareDirty(mRenderer, dirty.left, dirty.top, dirty.right, dirty.bottom, mOpaque);
} else {
@@ -259,12 +262,30 @@ class GLES20Canvas extends HardwareCanvas {
boolean opaque);
@Override
- void onPostDraw() {
+ public void onPostDraw() {
nFinish(mRenderer);
}
private static native void nFinish(int renderer);
+ /**
+ * Returns the size of the stencil buffer required by the underlying
+ * implementation.
+ *
+ * @return The minimum number of bits the stencil buffer must. Always >= 0.
+ *
+ * @hide
+ */
+ public static int getStencilSize() {
+ return nGetStencilSize();
+ }
+
+ private static native int nGetStencilSize();
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Functor
+ ///////////////////////////////////////////////////////////////////////////
+
@Override
public boolean callDrawGLFunction(int drawGLFunction) {
return nCallDrawGLFunction(mRenderer, drawGLFunction);
@@ -272,7 +293,6 @@ class GLES20Canvas extends HardwareCanvas {
private static native boolean nCallDrawGLFunction(int renderer, int drawGLFunction);
-
///////////////////////////////////////////////////////////////////////////
// Memory
///////////////////////////////////////////////////////////////////////////
@@ -405,12 +425,18 @@ class GLES20Canvas extends HardwareCanvas {
@Override
public boolean clipPath(Path path) {
- throw new UnsupportedOperationException();
+ // TODO: Implement
+ path.computeBounds(mPathBounds, true);
+ return nClipRect(mRenderer, mPathBounds.left, mPathBounds.top,
+ mPathBounds.right, mPathBounds.bottom, Region.Op.INTERSECT.nativeInt);
}
@Override
public boolean clipPath(Path path, Region.Op op) {
- throw new UnsupportedOperationException();
+ // TODO: Implement
+ path.computeBounds(mPathBounds, true);
+ return nClipRect(mRenderer, mPathBounds.left, mPathBounds.top,
+ mPathBounds.right, mPathBounds.bottom, op.nativeInt);
}
@Override
@@ -458,12 +484,18 @@ class GLES20Canvas extends HardwareCanvas {
@Override
public boolean clipRegion(Region region) {
- throw new UnsupportedOperationException();
+ // TODO: Implement
+ region.getBounds(mClipBounds);
+ return nClipRect(mRenderer, mClipBounds.left, mClipBounds.top,
+ mClipBounds.right, mClipBounds.bottom, Region.Op.INTERSECT.nativeInt);
}
@Override
public boolean clipRegion(Region region, Region.Op op) {
- throw new UnsupportedOperationException();
+ // TODO: Implement
+ region.getBounds(mClipBounds);
+ return nClipRect(mRenderer, mClipBounds.left, mClipBounds.top,
+ mClipBounds.right, mClipBounds.bottom, op.nativeInt);
}
@Override
@@ -483,12 +515,14 @@ class GLES20Canvas extends HardwareCanvas {
@Override
public boolean quickReject(Path path, EdgeType type) {
- throw new UnsupportedOperationException();
+ path.computeBounds(mPathBounds, true);
+ return nQuickReject(mRenderer, mPathBounds.left, mPathBounds.top,
+ mPathBounds.right, mPathBounds.bottom, type.nativeInt);
}
@Override
public boolean quickReject(RectF rect, EdgeType type) {
- return quickReject(rect.left, rect.top, rect.right, rect.bottom, type);
+ return nQuickReject(mRenderer, rect.left, rect.top, rect.right, rect.bottom, type.nativeInt);
}
///////////////////////////////////////////////////////////////////////////
@@ -525,11 +559,12 @@ class GLES20Canvas extends HardwareCanvas {
@Override
public void setMatrix(Matrix matrix) {
- nSetMatrix(mRenderer, matrix.native_instance);
+ nSetMatrix(mRenderer, matrix == null ? 0 : matrix.native_instance);
}
private static native void nSetMatrix(int renderer, int matrix);
+ @SuppressWarnings("deprecation")
@Override
public void getMatrix(Matrix matrix) {
nGetMatrix(mRenderer, matrix.native_instance);
@@ -642,8 +677,17 @@ class GLES20Canvas extends HardwareCanvas {
@Override
public void setDrawFilter(DrawFilter filter) {
mFilter = filter;
+ if (filter == null) {
+ nResetPaintFilter(mRenderer);
+ } else if (filter instanceof PaintFlagsDrawFilter) {
+ PaintFlagsDrawFilter flagsFilter = (PaintFlagsDrawFilter) filter;
+ nSetupPaintFilter(mRenderer, flagsFilter.clearBits, flagsFilter.setBits);
+ }
}
+ private static native void nResetPaintFilter(int renderer);
+ private static native void nSetupPaintFilter(int renderer, int clearBits, int setBits);
+
@Override
public DrawFilter getDrawFilter() {
return mFilter;
@@ -892,17 +936,42 @@ class GLES20Canvas extends HardwareCanvas {
@Override
public void drawPicture(Picture picture) {
- throw new UnsupportedOperationException();
+ if (picture.createdFromStream) {
+ return;
+ }
+
+ picture.endRecording();
+ // TODO: Implement rendering
}
@Override
public void drawPicture(Picture picture, Rect dst) {
- throw new UnsupportedOperationException();
+ if (picture.createdFromStream) {
+ return;
+ }
+
+ save();
+ translate(dst.left, dst.top);
+ if (picture.getWidth() > 0 && picture.getHeight() > 0) {
+ scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight());
+ }
+ drawPicture(picture);
+ restore();
}
@Override
public void drawPicture(Picture picture, RectF dst) {
- throw new UnsupportedOperationException();
+ if (picture.createdFromStream) {
+ return;
+ }
+
+ save();
+ translate(dst.left, dst.top);
+ if (picture.getWidth() > 0 && picture.getHeight() > 0) {
+ scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight());
+ }
+ drawPicture(picture);
+ restore();
}
@Override
@@ -927,16 +996,42 @@ class GLES20Canvas extends HardwareCanvas {
private static native void nDrawPoints(int renderer, float[] points,
int offset, int count, int paint);
+ @SuppressWarnings("deprecation")
@Override
public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) {
- // TODO: Implement
+ if (index < 0 || index + count > text.length || count * 2 > pos.length) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ int modifiers = setupModifiers(paint);
+ try {
+ nDrawPosText(mRenderer, text, index, count, pos, paint.mNativePaint);
+ } finally {
+ if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers);
+ }
}
+ private static native void nDrawPosText(int renderer, char[] text, int index, int count,
+ float[] pos, int paint);
+
+ @SuppressWarnings("deprecation")
@Override
public void drawPosText(String text, float[] pos, Paint paint) {
- // TODO: Implement
+ if (text.length() * 2 > pos.length) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ int modifiers = setupModifiers(paint);
+ try {
+ nDrawPosText(mRenderer, text, 0, text.length(), pos, paint.mNativePaint);
+ } finally {
+ if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers);
+ }
}
+ private static native void nDrawPosText(int renderer, String text, int start, int end,
+ float[] pos, int paint);
+
@Override
public void drawRect(float left, float top, float right, float bottom, Paint paint) {
int modifiers = setupModifiers(paint);
diff --git a/core/java/android/view/GLES20DisplayList.java b/core/java/android/view/GLES20DisplayList.java
index 4ca5e98..0cb9449 100644
--- a/core/java/android/view/GLES20DisplayList.java
+++ b/core/java/android/view/GLES20DisplayList.java
@@ -43,7 +43,7 @@ class GLES20DisplayList extends DisplayList {
}
@Override
- HardwareCanvas start() {
+ public HardwareCanvas start() {
if (mCanvas != null) {
throw new IllegalStateException("Recording has already started");
}
@@ -55,7 +55,7 @@ class GLES20DisplayList extends DisplayList {
}
@Override
- void invalidate() {
+ public void invalidate() {
if (mCanvas != null) {
mCanvas.recycle();
mCanvas = null;
@@ -64,12 +64,12 @@ class GLES20DisplayList extends DisplayList {
}
@Override
- boolean isValid() {
+ public boolean isValid() {
return mValid;
}
@Override
- void end() {
+ public void end() {
if (mCanvas != null) {
if (mFinalizer != null) {
mCanvas.end(mFinalizer.mNativeDisplayList);
@@ -83,7 +83,7 @@ class GLES20DisplayList extends DisplayList {
}
@Override
- int getSize() {
+ public int getSize() {
if (mFinalizer == null) return 0;
return GLES20Canvas.getDisplayListSize(mFinalizer.mNativeDisplayList);
}
diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java
index fd3b9e5..4f25792 100644
--- a/core/java/android/view/GLES20Layer.java
+++ b/core/java/android/view/GLES20Layer.java
@@ -60,6 +60,13 @@ abstract class GLES20Layer extends HardwareLayer {
}
mLayer = 0;
}
+
+ @Override
+ void flush() {
+ if (mLayer != 0) {
+ GLES20Canvas.nFlushLayer(mLayer);
+ }
+ }
static class Finalizer {
private int mLayerId;
diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java
index a496a9e..25d08ac 100644
--- a/core/java/android/view/GestureDetector.java
+++ b/core/java/android/view/GestureDetector.java
@@ -193,10 +193,8 @@ public class GestureDetector {
}
}
- // TODO: ViewConfiguration
- private int mBiggerTouchSlopSquare = 20 * 20;
-
private int mTouchSlopSquare;
+ private int mDoubleTapTouchSlopSquare;
private int mDoubleTapSlopSquare;
private int mMinimumFlingVelocity;
private int mMaximumFlingVelocity;
@@ -391,10 +389,11 @@ public class GestureDetector {
mIgnoreMultitouch = ignoreMultitouch;
// Fallback to support pre-donuts releases
- int touchSlop, doubleTapSlop;
+ int touchSlop, doubleTapSlop, doubleTapTouchSlop;
if (context == null) {
//noinspection deprecation
touchSlop = ViewConfiguration.getTouchSlop();
+ doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
//noinspection deprecation
mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
@@ -402,11 +401,13 @@ public class GestureDetector {
} else {
final ViewConfiguration configuration = ViewConfiguration.get(context);
touchSlop = configuration.getScaledTouchSlop();
+ doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
doubleTapSlop = configuration.getScaledDoubleTapSlop();
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
}
mTouchSlopSquare = touchSlop * touchSlop;
+ mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
}
@@ -545,7 +546,7 @@ public class GestureDetector {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
- if (distance > mBiggerTouchSlopSquare) {
+ if (distance > mDoubleTapTouchSlopSquare) {
mAlwaysInBiggerTapRegion = false;
}
} else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java
index 23b3abc..cbdbbde 100644
--- a/core/java/android/view/HardwareCanvas.java
+++ b/core/java/android/view/HardwareCanvas.java
@@ -42,12 +42,12 @@ public abstract class HardwareCanvas extends Canvas {
*
* @param dirty The dirty rectangle to update, can be null.
*/
- abstract void onPreDraw(Rect dirty);
+ public abstract void onPreDraw(Rect dirty);
/**
* Invoked after all drawing operation have been performed.
*/
- abstract void onPostDraw();
+ public abstract void onPostDraw();
/**
* Draws the specified display list onto this canvas.
@@ -61,7 +61,7 @@ public abstract class HardwareCanvas extends Canvas {
* @return True if the content of the display list requires another
* drawing pass (invalidate()), false otherwise
*/
- abstract boolean drawDisplayList(DisplayList displayList, int width, int height, Rect dirty);
+ public abstract boolean drawDisplayList(DisplayList displayList, int width, int height, Rect dirty);
/**
* Outputs the specified display list to the log. This method exists for use by
diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java
index 28389ab..d5666f3 100644
--- a/core/java/android/view/HardwareLayer.java
+++ b/core/java/android/view/HardwareLayer.java
@@ -116,6 +116,11 @@ abstract class HardwareLayer {
abstract void destroy();
/**
+ * Flush the render queue associated with this layer.
+ */
+ abstract void flush();
+
+ /**
* This must be invoked before drawing onto this layer.
* @param currentCanvas
*/
diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java
index ccb6489..e0749de 100644
--- a/core/java/android/view/HardwareRenderer.java
+++ b/core/java/android/view/HardwareRenderer.java
@@ -238,6 +238,15 @@ public abstract class HardwareRenderer {
private static native void nSetupShadersDiskCache(String cacheFile);
/**
+ * Notifies EGL that the frame is about to be rendered.
+ */
+ private static void beginFrame() {
+ nBeginFrame();
+ }
+
+ private static native void nBeginFrame();
+
+ /**
* Interface used to receive callbacks whenever a view is drawn by
* a hardware renderer instance.
*/
@@ -276,7 +285,7 @@ public abstract class HardwareRenderer {
*
* @return A new display list.
*/
- abstract DisplayList createDisplayList();
+ public abstract DisplayList createDisplayList();
/**
* Creates a new hardware layer. A hardware layer built by calling this
@@ -316,14 +325,13 @@ public abstract class HardwareRenderer {
* 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
+ * @param holder The target surface
*/
- void initializeIfNeeded(int width, int height, View.AttachInfo attachInfo,
- SurfaceHolder holder) throws Surface.OutOfResourcesException {
+ void initializeIfNeeded(int width, int height, SurfaceHolder holder)
+ throws Surface.OutOfResourcesException {
if (isRequested()) {
// We lost the gl context, so recreate it.
if (!isEnabled()) {
@@ -441,6 +449,8 @@ public abstract class HardwareRenderer {
}
boolean mDirtyRegionsEnabled;
+ boolean mUpdateDirtyRegions;
+
final boolean mVsyncDisabled;
final int mGlVersion;
@@ -675,6 +685,12 @@ public abstract class HardwareRenderer {
initCaches();
+ enableDirtyRegions();
+
+ return mEglContext.getGL();
+ }
+
+ private void enableDirtyRegions() {
// If mDirtyRegions is set, this means we have an EGL configuration
// with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set
if (sDirtyRegions) {
@@ -690,8 +706,6 @@ public abstract class HardwareRenderer {
// configuration (see RENDER_DIRTY_REGIONS)
mDirtyRegionsEnabled = GLES20Canvas.isBackBufferPreserved();
}
-
- return mEglContext.getGL();
}
abstract void initCaches();
@@ -745,6 +759,9 @@ public abstract class HardwareRenderer {
if (!createSurface(holder)) {
return;
}
+
+ mUpdateDirtyRegions = true;
+
if (mCanvas != null) {
setEnabled(true);
}
@@ -800,6 +817,7 @@ public abstract class HardwareRenderer {
}
void onPreDraw(Rect dirty) {
+
}
void onPostDraw() {
@@ -824,6 +842,8 @@ public abstract class HardwareRenderer {
dirty = null;
}
+ beginFrame();
+
onPreDraw(dirty);
HardwareCanvas canvas = mCanvas;
@@ -837,10 +857,37 @@ public abstract class HardwareRenderer {
(view.mPrivateFlags & View.INVALIDATED) == View.INVALIDATED;
view.mPrivateFlags &= ~View.INVALIDATED;
+ final long getDisplayListStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ getDisplayListStartTime = System.nanoTime();
+ }
+
DisplayList displayList = view.getDisplayList();
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- getDisplayList() took "
+ + ((now - getDisplayListStartTime) * 0.000001f) + "ms");
+ }
+
if (displayList != null) {
- if (canvas.drawDisplayList(displayList, view.getWidth(),
- view.getHeight(), mRedrawClip)) {
+ final long drawDisplayListStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ drawDisplayListStartTime = System.nanoTime();
+ }
+
+ boolean invalidateNeeded = canvas.drawDisplayList(
+ displayList, view.getWidth(),
+ view.getHeight(), mRedrawClip);
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- drawDisplayList() took "
+ + ((now - drawDisplayListStartTime) * 0.000001f)
+ + "ms, invalidateNeeded=" + invalidateNeeded + ".");
+ }
+
+ if (invalidateNeeded) {
if (mRedrawClip.isEmpty() || view.getParent() == null) {
view.invalidate();
} else {
@@ -872,7 +919,19 @@ public abstract class HardwareRenderer {
attachInfo.mIgnoreDirtyState = false;
+ final long eglSwapBuffersStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ eglSwapBuffersStartTime = System.nanoTime();
+ }
+
sEgl.eglSwapBuffers(sEglDisplay, mEglSurface);
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- eglSwapBuffers() took "
+ + ((now - eglSwapBuffersStartTime) * 0.000001f) + "ms");
+ }
+
checkEglErrors();
return dirty == null;
@@ -904,6 +963,10 @@ public abstract class HardwareRenderer {
fallback(true);
return SURFACE_STATE_ERROR;
} else {
+ if (mUpdateDirtyRegions) {
+ enableDirtyRegions();
+ mUpdateDirtyRegions = false;
+ }
return SURFACE_STATE_UPDATED;
}
}
@@ -984,7 +1047,7 @@ public abstract class HardwareRenderer {
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 0,
- EGL_STENCIL_SIZE, 0,
+ EGL_STENCIL_SIZE, GLES20Canvas.getStencilSize(),
EGL_SURFACE_TYPE, EGL_WINDOW_BIT |
(dirtyRegions ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0),
EGL_NONE
@@ -1031,7 +1094,7 @@ public abstract class HardwareRenderer {
}
@Override
- DisplayList createDisplayList() {
+ public DisplayList createDisplayList() {
return new GLES20DisplayList();
}
diff --git a/core/java/android/view/InputEvent.java b/core/java/android/view/InputEvent.java
index 01ddcc9..5602436 100755
--- a/core/java/android/view/InputEvent.java
+++ b/core/java/android/view/InputEvent.java
@@ -19,6 +19,8 @@ package android.view;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.concurrent.atomic.AtomicInteger;
+
/**
* Common base class for input events.
*/
@@ -27,8 +29,21 @@ public abstract class InputEvent implements Parcelable {
protected static final int PARCEL_TOKEN_MOTION_EVENT = 1;
/** @hide */
protected static final int PARCEL_TOKEN_KEY_EVENT = 2;
-
+
+ // Next sequence number.
+ private static final AtomicInteger mNextSeq = new AtomicInteger();
+
+ /** @hide */
+ protected int mSeq;
+
+ /** @hide */
+ protected boolean mRecycled;
+
+ private static final boolean TRACK_RECYCLED_LOCATION = false;
+ private RuntimeException mRecycledLocation;
+
/*package*/ InputEvent() {
+ mSeq = mNextSeq.getAndIncrement();
}
/**
@@ -82,7 +97,44 @@ public abstract class InputEvent implements Parcelable {
* objects are fine. See {@link KeyEvent#recycle()} for details.
* @hide
*/
- public abstract void recycle();
+ public void recycle() {
+ if (TRACK_RECYCLED_LOCATION) {
+ if (mRecycledLocation != null) {
+ throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation);
+ }
+ mRecycledLocation = new RuntimeException("Last recycled here");
+ } else {
+ if (mRecycled) {
+ throw new RuntimeException(toString() + " recycled twice!");
+ }
+ mRecycled = true;
+ }
+ }
+
+ /**
+ * Conditionally recycled the event if it is appropriate to do so after
+ * dispatching the event to an application.
+ *
+ * If the event is a {@link MotionEvent} then it is recycled.
+ *
+ * If the event is a {@link KeyEvent} then it is NOT recycled, because applications
+ * expect key events to be immutable so once the event has been dispatched to
+ * the application we can no longer recycle it.
+ * @hide
+ */
+ public void recycleIfNeededAfterDispatch() {
+ recycle();
+ }
+
+ /**
+ * Reinitializes the event on reuse (after recycling).
+ * @hide
+ */
+ protected void prepareForReuse() {
+ mRecycled = false;
+ mRecycledLocation = null;
+ mSeq = mNextSeq.getAndIncrement();
+ }
/**
* Gets a private flag that indicates when the system has detected that this input event
@@ -113,6 +165,22 @@ public abstract class InputEvent implements Parcelable {
*/
public abstract long getEventTimeNano();
+ /**
+ * Gets the unique sequence number of this event.
+ * Every input event that is created or received by a process has a
+ * unique sequence number. Moreover, a new sequence number is obtained
+ * each time an event object is recycled.
+ *
+ * Sequence numbers are only guaranteed to be locally unique within a process.
+ * Sequence numbers are not preserved when events are parceled.
+ *
+ * @return The unique sequence number of this event.
+ * @hide
+ */
+ public int getSequenceNumber() {
+ return mSeq;
+ }
+
public int describeContents() {
return 0;
}
diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java
index 9b081b2..fafe416 100644
--- a/core/java/android/view/InputEventConsistencyVerifier.java
+++ b/core/java/android/view/InputEventConsistencyVerifier.java
@@ -58,7 +58,7 @@ public final class InputEventConsistencyVerifier {
// so that the verifier can detect when it has been asked to verify the same event twice.
// It does not make sense to examine the contents of the last event since it may have
// been recycled.
- private InputEvent mLastEvent;
+ private int mLastEventSeq;
private String mLastEventType;
private int mLastNestingLevel;
@@ -140,7 +140,7 @@ public final class InputEventConsistencyVerifier {
* Resets the state of the input event consistency verifier.
*/
public void reset() {
- mLastEvent = null;
+ mLastEventSeq = -1;
mLastNestingLevel = 0;
mTrackballDown = false;
mTrackballUnhandled = false;
@@ -573,17 +573,18 @@ public final class InputEventConsistencyVerifier {
private boolean startEvent(InputEvent event, int nestingLevel, String eventType) {
// Ignore the event if we already checked it at a higher nesting level.
- if (event == mLastEvent && nestingLevel < mLastNestingLevel
+ final int seq = event.getSequenceNumber();
+ if (seq == mLastEventSeq && nestingLevel < mLastNestingLevel
&& eventType == mLastEventType) {
return false;
}
if (nestingLevel > 0) {
- mLastEvent = event;
+ mLastEventSeq = seq;
mLastEventType = eventType;
mLastNestingLevel = nestingLevel;
} else {
- mLastEvent = null;
+ mLastEventSeq = -1;
mLastEventType = null;
mLastNestingLevel = 0;
}
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
new file mode 100644
index 0000000..764d8dc
--- /dev/null
+++ b/core/java/android/view/InputEventReceiver.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import dalvik.system.CloseGuard;
+
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+/**
+ * Provides a low-level mechanism for an application to receive input events.
+ * @hide
+ */
+public abstract class InputEventReceiver {
+ private static final String TAG = "InputEventReceiver";
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private int mReceiverPtr;
+
+ // We keep references to the input channel and message queue objects here so that
+ // they are not GC'd while the native peer of the receiver is using them.
+ private InputChannel mInputChannel;
+ private MessageQueue mMessageQueue;
+
+ // The sequence number of the event that is in progress.
+ private int mEventSequenceNumberInProgress = -1;
+
+ private static native int nativeInit(InputEventReceiver receiver,
+ InputChannel inputChannel, MessageQueue messageQueue);
+ private static native void nativeDispose(int receiverPtr);
+ private static native void nativeFinishInputEvent(int receiverPtr, boolean handled);
+
+ /**
+ * Creates an input event receiver bound to the specified input channel.
+ *
+ * @param inputChannel The input channel.
+ * @param looper The looper to use when invoking callbacks.
+ */
+ public InputEventReceiver(InputChannel inputChannel, Looper looper) {
+ if (inputChannel == null) {
+ throw new IllegalArgumentException("inputChannel must not be null");
+ }
+ if (looper == null) {
+ throw new IllegalArgumentException("looper must not be null");
+ }
+
+ mInputChannel = inputChannel;
+ mMessageQueue = looper.getQueue();
+ mReceiverPtr = nativeInit(this, inputChannel, mMessageQueue);
+
+ mCloseGuard.open("dispose");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Disposes the receiver.
+ */
+ public void dispose() {
+ if (mCloseGuard != null) {
+ mCloseGuard.close();
+ }
+ if (mReceiverPtr != 0) {
+ nativeDispose(mReceiverPtr);
+ mReceiverPtr = 0;
+ }
+ mInputChannel = null;
+ mMessageQueue = null;
+ }
+
+ /**
+ * Called when an input event is received.
+ * The recipient should process the input event and then call {@link #finishInputEvent}
+ * to indicate whether the event was handled. No new input events will be received
+ * until {@link #finishInputEvent} is called.
+ *
+ * @param event The input event that was received.
+ */
+ public void onInputEvent(InputEvent event) {
+ finishInputEvent(event, false);
+ }
+
+ /**
+ * Finishes an input event and indicates whether it was handled.
+ *
+ * @param event The input event that was finished.
+ * @param handled True if the event was handled.
+ */
+ public void finishInputEvent(InputEvent event, boolean handled) {
+ if (event == null) {
+ throw new IllegalArgumentException("event must not be null");
+ }
+ if (mReceiverPtr == 0) {
+ Log.w(TAG, "Attempted to finish an input event but the input event "
+ + "receiver has already been disposed.");
+ } else {
+ if (event.getSequenceNumber() != mEventSequenceNumberInProgress) {
+ Log.w(TAG, "Attempted to finish an input event that is not in progress.");
+ } else {
+ mEventSequenceNumberInProgress = -1;
+ nativeFinishInputEvent(mReceiverPtr, handled);
+ }
+ }
+ event.recycleIfNeededAfterDispatch();
+ }
+
+ // Called from native code.
+ @SuppressWarnings("unused")
+ private void dispatchInputEvent(InputEvent event) {
+ mEventSequenceNumberInProgress = event.getSequenceNumber();
+ onInputEvent(event);
+ }
+
+ public static interface Factory {
+ public InputEventReceiver createInputEventReceiver(
+ InputChannel inputChannel, Looper looper);
+ }
+}
diff --git a/core/java/android/view/InputHandler.java b/core/java/android/view/InputHandler.java
deleted file mode 100644
index 14ce14c..0000000
--- a/core/java/android/view/InputHandler.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view;
-
-/**
- * Handles input messages that arrive on an input channel.
- * @hide
- */
-public interface InputHandler {
- /**
- * Handle a key event.
- * It is the responsibility of the callee to ensure that the finished callback is
- * eventually invoked when the event processing is finished and the input system
- * can send the next event.
- * @param event The key event data.
- * @param finishedCallback The callback to invoke when event processing is finished.
- */
- public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback);
-
- /**
- * Handle a motion event.
- * It is the responsibility of the callee to ensure that the finished callback is
- * eventually invoked when the event processing is finished and the input system
- * can send the next event.
- * @param event The motion event data.
- * @param finishedCallback The callback to invoke when event processing is finished.
- */
- public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback);
-}
diff --git a/core/java/android/view/InputQueue.java b/core/java/android/view/InputQueue.java
index 5735b63..909a3b2 100644
--- a/core/java/android/view/InputQueue.java
+++ b/core/java/android/view/InputQueue.java
@@ -16,18 +16,11 @@
package android.view;
-import android.os.MessageQueue;
-import android.util.Slog;
-
/**
* An input queue provides a mechanism for an application to receive incoming
* input events. Currently only usable from native code.
*/
public final class InputQueue {
- private static final String TAG = "InputQueue";
-
- private static final boolean DEBUG = false;
-
/**
* Interface to receive notification of when an InputQueue is associated
* and dissociated with a thread.
@@ -48,13 +41,6 @@ public final class InputQueue {
final InputChannel mChannel;
- private static final Object sLock = new Object();
-
- private static native void nativeRegisterInputChannel(InputChannel inputChannel,
- InputHandler inputHandler, MessageQueue messageQueue);
- private static native void nativeUnregisterInputChannel(InputChannel inputChannel);
- private static native void nativeFinished(long finishedToken, boolean handled);
-
/** @hide */
public InputQueue(InputChannel channel) {
mChannel = channel;
@@ -64,121 +50,4 @@ public final class InputQueue {
public InputChannel getInputChannel() {
return mChannel;
}
-
- /**
- * Registers an input channel and handler.
- * @param inputChannel The input channel to register.
- * @param inputHandler The input handler to input events send to the target.
- * @param messageQueue The message queue on whose thread the handler should be invoked.
- * @hide
- */
- public static void registerInputChannel(InputChannel inputChannel, InputHandler inputHandler,
- MessageQueue messageQueue) {
- if (inputChannel == null) {
- throw new IllegalArgumentException("inputChannel must not be null");
- }
- if (inputHandler == null) {
- throw new IllegalArgumentException("inputHandler must not be null");
- }
- if (messageQueue == null) {
- throw new IllegalArgumentException("messageQueue must not be null");
- }
-
- synchronized (sLock) {
- if (DEBUG) {
- Slog.d(TAG, "Registering input channel '" + inputChannel + "'");
- }
-
- nativeRegisterInputChannel(inputChannel, inputHandler, messageQueue);
- }
- }
-
- /**
- * Unregisters an input channel.
- * Does nothing if the channel is not currently registered.
- * @param inputChannel The input channel to unregister.
- * @hide
- */
- public static void unregisterInputChannel(InputChannel inputChannel) {
- if (inputChannel == null) {
- throw new IllegalArgumentException("inputChannel must not be null");
- }
-
- synchronized (sLock) {
- if (DEBUG) {
- Slog.d(TAG, "Unregistering input channel '" + inputChannel + "'");
- }
-
- nativeUnregisterInputChannel(inputChannel);
- }
- }
-
- @SuppressWarnings("unused")
- private static void dispatchKeyEvent(InputHandler inputHandler,
- KeyEvent event, long finishedToken) {
- FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken);
- inputHandler.handleKey(event, finishedCallback);
- }
-
- @SuppressWarnings("unused")
- private static void dispatchMotionEvent(InputHandler inputHandler,
- MotionEvent event, long finishedToken) {
- FinishedCallback finishedCallback = FinishedCallback.obtain(finishedToken);
- inputHandler.handleMotion(event, finishedCallback);
- }
-
- /**
- * A callback that must be invoked to when finished processing an event.
- * @hide
- */
- public static final class FinishedCallback {
- private static final boolean DEBUG_RECYCLING = false;
-
- private static final int RECYCLE_MAX_COUNT = 4;
-
- private static FinishedCallback sRecycleHead;
- private static int sRecycleCount;
-
- private FinishedCallback mRecycleNext;
- private long mFinishedToken;
-
- private FinishedCallback() {
- }
-
- public static FinishedCallback obtain(long finishedToken) {
- synchronized (sLock) {
- FinishedCallback callback = sRecycleHead;
- if (callback != null) {
- sRecycleHead = callback.mRecycleNext;
- sRecycleCount -= 1;
- callback.mRecycleNext = null;
- } else {
- callback = new FinishedCallback();
- }
- callback.mFinishedToken = finishedToken;
- return callback;
- }
- }
-
- public void finished(boolean handled) {
- synchronized (sLock) {
- if (mFinishedToken == -1) {
- throw new IllegalStateException("Event finished callback already invoked.");
- }
-
- nativeFinished(mFinishedToken, handled);
- mFinishedToken = -1;
-
- if (sRecycleCount < RECYCLE_MAX_COUNT) {
- mRecycleNext = sRecycleHead;
- sRecycleHead = this;
- sRecycleCount += 1;
-
- if (DEBUG_RECYCLING) {
- Slog.d(TAG, "Recycled finished callbacks: " + sRecycleCount);
- }
- }
- }
- }
- }
}
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index f53e42c..104ed6a 100755
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -1225,7 +1225,6 @@ public class KeyEvent extends InputEvent implements Parcelable {
private static KeyEvent gRecyclerTop;
private KeyEvent mNext;
- private boolean mRecycled;
private int mDeviceId;
private int mSource;
@@ -1535,8 +1534,8 @@ public class KeyEvent extends InputEvent implements Parcelable {
gRecyclerTop = ev.mNext;
gRecyclerUsed -= 1;
}
- ev.mRecycled = false;
ev.mNext = null;
+ ev.prepareForReuse();
return ev;
}
@@ -1597,11 +1596,9 @@ public class KeyEvent extends InputEvent implements Parcelable {
*
* @hide
*/
+ @Override
public final void recycle() {
- if (mRecycled) {
- throw new RuntimeException(toString() + " recycled twice!");
- }
- mRecycled = true;
+ super.recycle();
mCharacters = null;
synchronized (gRecyclerLock) {
@@ -1613,6 +1610,12 @@ public class KeyEvent extends InputEvent implements Parcelable {
}
}
+ /** @hide */
+ @Override
+ public final void recycleIfNeededAfterDispatch() {
+ // Do nothing.
+ }
+
/**
* Create a new key event that is the same as the given one, but whose
* event time and repeat count are replaced with the given value.
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 8e0ab1a..92e8f4e 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -167,7 +167,6 @@ import android.util.SparseArray;
*/
public final class MotionEvent extends InputEvent implements Parcelable {
private static final long NS_PER_MS = 1000000;
- private static final boolean TRACK_RECYCLED_LOCATION = false;
/**
* An invalid pointer id.
@@ -1315,8 +1314,6 @@ public final class MotionEvent extends InputEvent implements Parcelable {
private int mNativePtr;
private MotionEvent mNext;
- private RuntimeException mRecycledLocation;
- private boolean mRecycled;
private static native int nativeInitialize(int nativePtr,
int deviceId, int source, int action, int flags, int edgeFlags,
@@ -1397,9 +1394,8 @@ public final class MotionEvent extends InputEvent implements Parcelable {
gRecyclerTop = ev.mNext;
gRecyclerUsed -= 1;
}
- ev.mRecycledLocation = null;
- ev.mRecycled = false;
ev.mNext = null;
+ ev.prepareForReuse();
return ev;
}
@@ -1646,20 +1642,9 @@ public final class MotionEvent extends InputEvent implements Parcelable {
* Recycle the MotionEvent, to be re-used by a later caller. After calling
* this function you must not ever touch the event again.
*/
+ @Override
public final void recycle() {
- // Ensure recycle is only called once!
- if (TRACK_RECYCLED_LOCATION) {
- if (mRecycledLocation != null) {
- throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation);
- }
- mRecycledLocation = new RuntimeException("Last recycled here");
- //Log.w("MotionEvent", "Recycling event " + this, mRecycledLocation);
- } else {
- if (mRecycled) {
- throw new RuntimeException(toString() + " recycled twice!");
- }
- mRecycled = true;
- }
+ super.recycle();
synchronized (gRecyclerLock) {
if (gRecyclerUsed < MAX_RECYCLED) {
diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java
index dfb2c32..1c35e31 100644
--- a/core/java/android/view/VelocityTracker.java
+++ b/core/java/android/view/VelocityTracker.java
@@ -29,7 +29,7 @@ import android.util.PoolableManager;
* to begin tracking. Put the motion events you receive into it with
* {@link #addMovement(MotionEvent)}. When you want to determine the velocity call
* {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)}
- * and {@link #getXVelocity(int)} to retrieve the velocity for each pointer id.
+ * and {@link #getYVelocity(int)} to retrieve the velocity for each pointer id.
*/
public final class VelocityTracker implements Poolable<VelocityTracker> {
private static final Pool<VelocityTracker> sPool = Pools.synchronizedPool(
@@ -39,6 +39,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> {
}
public void onAcquired(VelocityTracker element) {
+ // Intentionally empty
}
public void onReleased(VelocityTracker element) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 54bb056..39f603d 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -62,6 +62,7 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityEventSource;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
@@ -1970,6 +1971,21 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 0x00000002;
/**
+ * Find views that contain {@link AccessibilityNodeProvider}. Such
+ * a View is a root of virtual view hierarchy and may contain the searched
+ * text. If this flag is set Views with providers are automatically
+ * added and it is a responsibility of the client to call the APIs of
+ * the provider to determine whether the virtual tree rooted at this View
+ * contains the text, i.e. getting the list of {@link AccessibilityNodeInfo}s
+ * represeting the virtual views with this text.
+ *
+ * @see #findViewsWithText(ArrayList, CharSequence, int)
+ *
+ * @hide
+ */
+ public static final int FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS = 0x00000004;
+
+ /**
* Controls the over-scroll mode for this view.
* See {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)},
* {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS},
@@ -2610,7 +2626,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
/**
* Text direction is using "first strong algorithm". The first strong directional character
* determines the paragraph direction. If there is no strong directional character, the
- * paragraph direction is the view's resolved ayout direction.
+ * paragraph direction is the view's resolved layout direction.
*
* @hide
*/
@@ -2640,6 +2656,13 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
public static final int TEXT_DIRECTION_RTL = 4;
/**
+ * Text direction is coming from the system Locale.
+ *
+ * @hide
+ */
+ public static final int TEXT_DIRECTION_LOCALE = 5;
+
+ /**
* Default text direction is inherited
*
* @hide
@@ -2656,13 +2679,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
@ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"),
@ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"),
@ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"),
- @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL")
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_LOCALE, to = "LOCALE")
})
private int mTextDirection = DEFAULT_TEXT_DIRECTION;
/**
* The resolved text direction. This needs resolution if the value is
- * TEXT_DIRECTION_INHERIT. The resolution matches mTextDirection if that is
+ * TEXT_DIRECTION_INHERIT. The resolution matches mTextDirection if it is
* not TEXT_DIRECTION_INHERIT, otherwise resolution proceeds up the parent
* chain of the view.
*
@@ -2673,7 +2697,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
@ViewDebug.IntToString(from = TEXT_DIRECTION_FIRST_STRONG, to = "FIRST_STRONG"),
@ViewDebug.IntToString(from = TEXT_DIRECTION_ANY_RTL, to = "ANY_RTL"),
@ViewDebug.IntToString(from = TEXT_DIRECTION_LTR, to = "LTR"),
- @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL")
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_RTL, to = "RTL"),
+ @ViewDebug.IntToString(from = TEXT_DIRECTION_LOCALE, to = "LOCALE")
})
private int mResolvedTextDirection = TEXT_DIRECTION_INHERIT;
@@ -4082,7 +4107,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
*/
void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
event.setSource(this);
- event.setClassName(getClass().getName());
+ event.setClassName(View.class.getName());
event.setPackageName(getContext().getPackageName());
event.setEnabled(isEnabled());
event.setContentDescription(mContentDescription);
@@ -4108,14 +4133,20 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* Note: The client is responsible for recycling the obtained instance by calling
* {@link AccessibilityNodeInfo#recycle()} to minimize object creation.
* </p>
+ *
* @return A populated {@link AccessibilityNodeInfo}.
*
* @see AccessibilityNodeInfo
*/
public AccessibilityNodeInfo createAccessibilityNodeInfo() {
- AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this);
- onInitializeAccessibilityNodeInfo(info);
- return info;
+ AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
+ if (provider != null) {
+ return provider.createAccessibilityNodeInfo(View.NO_ID);
+ } else {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this);
+ onInitializeAccessibilityNodeInfo(info);
+ return info;
+ }
}
/**
@@ -4181,7 +4212,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
}
info.setPackageName(mContext.getPackageName());
- info.setClassName(getClass().getName());
+ info.setClassName(View.class.getName());
info.setContentDescription(getContentDescription());
info.setEnabled(isEnabled());
@@ -4220,6 +4251,36 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
}
/**
+ * Gets the provider for managing a virtual view hierarchy rooted at this View
+ * and reported to {@link android.accessibilityservice.AccessibilityService}s
+ * that explore the window content.
+ * <p>
+ * If this method returns an instance, this instance is responsible for managing
+ * {@link AccessibilityNodeInfo}s describing the virtual sub-tree rooted at this
+ * View including the one representing the View itself. Similarly the returned
+ * instance is responsible for performing accessibility actions on any virtual
+ * view or the root view itself.
+ * </p>
+ * <p>
+ * If an {@link AccessibilityDelegate} has been specified via calling
+ * {@link #setAccessibilityDelegate(AccessibilityDelegate)} its
+ * {@link AccessibilityDelegate#getAccessibilityNodeProvider(View)}
+ * is responsible for handling this call.
+ * </p>
+ *
+ * @return The provider.
+ *
+ * @see AccessibilityNodeProvider
+ */
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ if (mAccessibilityDelegate != null) {
+ return mAccessibilityDelegate.getAccessibilityNodeProvider(this);
+ } else {
+ return null;
+ }
+ }
+
+ /**
* Gets the unique identifier of this view on the screen for accessibility purposes.
* If this {@link View} is not attached to any window, {@value #NO_ID} is returned.
*
@@ -5238,14 +5299,18 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
*
* @param outViews The output list of matching Views.
* @param searched The text to match against.
- *
+ *
* @see #FIND_VIEWS_WITH_TEXT
* @see #FIND_VIEWS_WITH_CONTENT_DESCRIPTION
* @see #setContentDescription(CharSequence)
*/
public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
- if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0 && !TextUtils.isEmpty(searched)
- && !TextUtils.isEmpty(mContentDescription)) {
+ if (getAccessibilityNodeProvider() != null) {
+ if ((flags & FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS) != 0) {
+ outViews.add(this);
+ }
+ } else if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0
+ && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mContentDescription)) {
String searchedLowerCase = searched.toString().toLowerCase();
String contentDescriptionLowerCase = mContentDescription.toString().toLowerCase();
if (contentDescriptionLowerCase.contains(searchedLowerCase)) {
@@ -6757,7 +6822,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
if ((changed & VISIBILITY_MASK) != 0) {
if (mParent instanceof ViewGroup) {
- ((ViewGroup) mParent).onChildVisibilityChanged(this, (flags & VISIBILITY_MASK));
+ ((ViewGroup) mParent).onChildVisibilityChanged(this, (changed & VISIBILITY_MASK),
+ (flags & VISIBILITY_MASK));
((View) mParent).invalidate(true);
} else if (mParent != null) {
mParent.invalidateChild(this, null);
@@ -7532,15 +7598,17 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
*/
public void setAlpha(float alpha) {
ensureTransformationInfo();
- mTransformationInfo.mAlpha = alpha;
- invalidateParentCaches();
- if (onSetAlpha((int) (alpha * 255))) {
- mPrivateFlags |= ALPHA_SET;
- // subclass is handling alpha - don't optimize rendering cache invalidation
- invalidate(true);
- } else {
- mPrivateFlags &= ~ALPHA_SET;
- invalidate(false);
+ if (mTransformationInfo.mAlpha != alpha) {
+ mTransformationInfo.mAlpha = alpha;
+ invalidateParentCaches();
+ if (onSetAlpha((int) (alpha * 255))) {
+ mPrivateFlags |= ALPHA_SET;
+ // subclass is handling alpha - don't optimize rendering cache invalidation
+ invalidate(true);
+ } else {
+ mPrivateFlags &= ~ALPHA_SET;
+ invalidate(false);
+ }
}
}
@@ -7551,18 +7619,22 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* alpha (the return value for onSetAlpha()).
*
* @param alpha The new value for the alpha property
- * @return true if the View subclass handles alpha (the return value for onSetAlpha())
+ * @return true if the View subclass handles alpha (the return value for onSetAlpha()) and
+ * the new value for the alpha property is different from the old value
*/
boolean setAlphaNoInvalidation(float alpha) {
ensureTransformationInfo();
- mTransformationInfo.mAlpha = alpha;
- boolean subclassHandlesAlpha = onSetAlpha((int) (alpha * 255));
- if (subclassHandlesAlpha) {
- mPrivateFlags |= ALPHA_SET;
- } else {
- mPrivateFlags &= ~ALPHA_SET;
+ if (mTransformationInfo.mAlpha != alpha) {
+ mTransformationInfo.mAlpha = alpha;
+ boolean subclassHandlesAlpha = onSetAlpha((int) (alpha * 255));
+ if (subclassHandlesAlpha) {
+ mPrivateFlags |= ALPHA_SET;
+ return true;
+ } else {
+ mPrivateFlags &= ~ALPHA_SET;
+ }
}
- return subclassHandlesAlpha;
+ return false;
}
/**
@@ -7925,84 +7997,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
}
/**
- * @hide
- */
- public void setFastTranslationX(float x) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mTranslationX = x;
- info.mMatrixDirty = true;
- }
-
- /**
- * @hide
- */
- public void setFastTranslationY(float y) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mTranslationY = y;
- info.mMatrixDirty = true;
- }
-
- /**
- * @hide
- */
- public void setFastX(float x) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mTranslationX = x - mLeft;
- info.mMatrixDirty = true;
- }
-
- /**
- * @hide
- */
- public void setFastY(float y) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mTranslationY = y - mTop;
- info.mMatrixDirty = true;
- }
-
- /**
- * @hide
- */
- public void setFastScaleX(float x) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mScaleX = x;
- info.mMatrixDirty = true;
- }
-
- /**
- * @hide
- */
- public void setFastScaleY(float y) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mScaleY = y;
- info.mMatrixDirty = true;
- }
-
- /**
- * @hide
- */
- public void setFastAlpha(float alpha) {
- ensureTransformationInfo();
- mTransformationInfo.mAlpha = alpha;
- }
-
- /**
- * @hide
- */
- public void setFastRotationY(float y) {
- ensureTransformationInfo();
- final TransformationInfo info = mTransformationInfo;
- info.mRotationY = y;
- info.mMatrixDirty = true;
- }
-
- /**
* Hit rectangle in parent's coordinates
*
* @param outRect The hit rectangle of the view.
@@ -8579,37 +8573,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
}
/**
- * @hide
- */
- public void fastInvalidate() {
- if (skipInvalidate()) {
- return;
- }
- if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) ||
- (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID ||
- (mPrivateFlags & INVALIDATED) != INVALIDATED) {
- if (mParent instanceof View) {
- ((View) mParent).mPrivateFlags |= INVALIDATED;
- }
- mPrivateFlags &= ~DRAWN;
- mPrivateFlags |= DIRTY;
- mPrivateFlags |= INVALIDATED;
- mPrivateFlags &= ~DRAWING_CACHE_VALID;
- if (mParent != null && mAttachInfo != null) {
- if (mAttachInfo.mHardwareAccelerated) {
- mParent.invalidateChild(this, null);
- } else {
- final Rect r = mAttachInfo.mTmpInvalRect;
- r.set(0, 0, mRight - mLeft, mBottom - mTop);
- // Don't call invalidate -- we don't want to internally scroll
- // our own bounds
- mParent.invalidateChild(this, r);
- }
- }
- }
- }
-
- /**
* Used to indicate that the parent of this view should clear its caches. This functionality
* is used to force the parent to rebuild its display list (when hardware-accelerated),
* which is necessary when various parent-managed properties of the view change, such as
@@ -10151,6 +10114,13 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
break;
}
}
+
+ // Make sure the HardwareRenderer.validate() was invoked before calling this method
+ void flushLayer() {
+ if (mLayerType == LAYER_TYPE_HARDWARE && mHardwareLayer != null) {
+ mHardwareLayer.flush();
+ }
+ }
/**
* <p>Returns a hardware layer that can be used to draw this view again
@@ -10163,6 +10133,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
!mAttachInfo.mHardwareRenderer.isEnabled()) {
return null;
}
+
+ if (!mAttachInfo.mHardwareRenderer.validate()) return null;
final int width = mRight - mLeft;
final int height = mBottom - mTop;
@@ -10237,12 +10209,15 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
*/
boolean destroyLayer() {
if (mHardwareLayer != null) {
- mHardwareLayer.destroy();
- mHardwareLayer = null;
-
- invalidate(true);
- invalidateParentCaches();
+ AttachInfo info = mAttachInfo;
+ if (info != null && info.mHardwareRenderer != null &&
+ info.mHardwareRenderer.isEnabled() && info.mHardwareRenderer.validate()) {
+ mHardwareLayer.destroy();
+ mHardwareLayer = null;
+ invalidate(true);
+ invalidateParentCaches();
+ }
return true;
}
return false;
@@ -10356,6 +10331,19 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
}
/**
+ * @return The HardwareRenderer associated with that view or null if hardware rendering
+ * is not supported or this this has not been attached to a window.
+ *
+ * @hide
+ */
+ public HardwareRenderer getHardwareRenderer() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mHardwareRenderer;
+ }
+ return null;
+ }
+
+ /**
* <p>Returns a display list that can be used to draw this view again
* without executing its draw method.</p>
*
@@ -12152,13 +12140,16 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* @param location an array of two integers in which to hold the coordinates
*/
public void getLocationInWindow(int[] location) {
- // When the view is not attached to a window, this method does not make sense
- if (mAttachInfo == null) return;
-
if (location == null || location.length < 2) {
throw new IllegalArgumentException("location must be an array of two integers");
}
+ if (mAttachInfo == null) {
+ // When the view is not attached to a window, this method does not make sense
+ location[0] = location[1] = 0;
+ return;
+ }
+
float[] position = mAttachInfo.mTmpTransformLocation;
position[0] = position[1] = 0.0f;
@@ -13720,6 +13711,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* {@link #TEXT_DIRECTION_ANY_RTL},
* {@link #TEXT_DIRECTION_LTR},
* {@link #TEXT_DIRECTION_RTL},
+ * {@link #TEXT_DIRECTION_LOCALE},
*
* @hide
*/
@@ -13737,6 +13729,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* {@link #TEXT_DIRECTION_ANY_RTL},
* {@link #TEXT_DIRECTION_LTR},
* {@link #TEXT_DIRECTION_RTL},
+ * {@link #TEXT_DIRECTION_LOCALE},
*
* @hide
*/
@@ -13757,6 +13750,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* {@link #TEXT_DIRECTION_ANY_RTL},
* {@link #TEXT_DIRECTION_LTR},
* {@link #TEXT_DIRECTION_RTL},
+ * {@link #TEXT_DIRECTION_LOCALE},
*
* @hide
*/
@@ -13800,7 +13794,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>alpha</code> functionality handled by the
* {@link View#setAlpha(float)} and {@link View#getAlpha()} methods.
*/
- public static Property<View, Float> ALPHA = new FloatProperty<View>("alpha") {
+ public static final Property<View, Float> ALPHA = new FloatProperty<View>("alpha") {
@Override
public void setValue(View object, float value) {
object.setAlpha(value);
@@ -13816,7 +13810,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>translationX</code> functionality handled by the
* {@link View#setTranslationX(float)} and {@link View#getTranslationX()} methods.
*/
- public static Property<View, Float> TRANSLATION_X = new FloatProperty<View>("translationX") {
+ public static final Property<View, Float> TRANSLATION_X = new FloatProperty<View>("translationX") {
@Override
public void setValue(View object, float value) {
object.setTranslationX(value);
@@ -13832,7 +13826,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>translationY</code> functionality handled by the
* {@link View#setTranslationY(float)} and {@link View#getTranslationY()} methods.
*/
- public static Property<View, Float> TRANSLATION_Y = new FloatProperty<View>("translationY") {
+ public static final Property<View, Float> TRANSLATION_Y = new FloatProperty<View>("translationY") {
@Override
public void setValue(View object, float value) {
object.setTranslationY(value);
@@ -13848,7 +13842,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>x</code> functionality handled by the
* {@link View#setX(float)} and {@link View#getX()} methods.
*/
- public static Property<View, Float> X = new FloatProperty<View>("x") {
+ public static final Property<View, Float> X = new FloatProperty<View>("x") {
@Override
public void setValue(View object, float value) {
object.setX(value);
@@ -13864,7 +13858,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>y</code> functionality handled by the
* {@link View#setY(float)} and {@link View#getY()} methods.
*/
- public static Property<View, Float> Y = new FloatProperty<View>("y") {
+ public static final Property<View, Float> Y = new FloatProperty<View>("y") {
@Override
public void setValue(View object, float value) {
object.setY(value);
@@ -13880,7 +13874,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>rotation</code> functionality handled by the
* {@link View#setRotation(float)} and {@link View#getRotation()} methods.
*/
- public static Property<View, Float> ROTATION = new FloatProperty<View>("rotation") {
+ public static final Property<View, Float> ROTATION = new FloatProperty<View>("rotation") {
@Override
public void setValue(View object, float value) {
object.setRotation(value);
@@ -13896,7 +13890,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>rotationX</code> functionality handled by the
* {@link View#setRotationX(float)} and {@link View#getRotationX()} methods.
*/
- public static Property<View, Float> ROTATION_X = new FloatProperty<View>("rotationX") {
+ public static final Property<View, Float> ROTATION_X = new FloatProperty<View>("rotationX") {
@Override
public void setValue(View object, float value) {
object.setRotationX(value);
@@ -13912,7 +13906,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>rotationY</code> functionality handled by the
* {@link View#setRotationY(float)} and {@link View#getRotationY()} methods.
*/
- public static Property<View, Float> ROTATION_Y = new FloatProperty<View>("rotationY") {
+ public static final Property<View, Float> ROTATION_Y = new FloatProperty<View>("rotationY") {
@Override
public void setValue(View object, float value) {
object.setRotationY(value);
@@ -13928,7 +13922,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>scaleX</code> functionality handled by the
* {@link View#setScaleX(float)} and {@link View#getScaleX()} methods.
*/
- public static Property<View, Float> SCALE_X = new FloatProperty<View>("scaleX") {
+ public static final Property<View, Float> SCALE_X = new FloatProperty<View>("scaleX") {
@Override
public void setValue(View object, float value) {
object.setScaleX(value);
@@ -13944,7 +13938,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
* A Property wrapper around the <code>scaleY</code> functionality handled by the
* {@link View#setScaleY(float)} and {@link View#getScaleY()} methods.
*/
- public static Property<View, Float> SCALE_Y = new FloatProperty<View>("scaleY") {
+ public static final Property<View, Float> SCALE_Y = new FloatProperty<View>("scaleY") {
@Override
public void setValue(View object, float value) {
object.setScaleY(value);
@@ -14995,5 +14989,23 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
AccessibilityEvent event) {
return host.onRequestSendAccessibilityEventInternal(child, event);
}
+
+ /**
+ * Gets the provider for managing a virtual view hierarchy rooted at this View
+ * and reported to {@link android.accessibilityservice.AccessibilityService}s
+ * that explore the window content.
+ * <p>
+ * The default implementation behaves as
+ * {@link View#getAccessibilityNodeProvider() View#getAccessibilityNodeProvider()} for
+ * the case of no accessibility delegate been set.
+ * </p>
+ *
+ * @return The provider.
+ *
+ * @see AccessibilityNodeProvider
+ */
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ return null;
+ }
}
}
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 9bd42ef..b455ad5 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -49,7 +49,7 @@ public class ViewConfiguration {
/**
* Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
- * pixels
+ * dips
*/
private static final int SCROLL_BAR_SIZE = 10;
@@ -64,7 +64,7 @@ public class ViewConfiguration {
private static final int SCROLL_BAR_DEFAULT_DELAY = 300;
/**
- * Defines the length of the fading edges in pixels
+ * Defines the length of the fading edges in dips
*/
private static final int FADING_EDGE_LENGTH = 12;
@@ -134,7 +134,7 @@ public class ViewConfiguration {
private static final int ZOOM_CONTROLS_TIMEOUT = 3000;
/**
- * Inset in pixels to look for touchable content when the user touches the edge of the screen
+ * Inset in dips to look for touchable content when the user touches the edge of the screen
*/
private static final int EDGE_SLOP = 12;
@@ -152,6 +152,12 @@ public class ViewConfiguration {
private static final int TOUCH_SLOP = 8;
/**
+ * Distance the first touch can wander before we stop considering this event a double tap
+ * (in dips)
+ */
+ private static final int DOUBLE_TAP_TOUCH_SLOP = TOUCH_SLOP;
+
+ /**
* Distance a touch can wander before we think the user is attempting a paged scroll
* (in dips)
*
@@ -166,28 +172,28 @@ public class ViewConfiguration {
private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2;
/**
- * Distance between the first touch and second touch to still be considered a double tap
+ * Distance in dips between the first touch and second touch to still be considered a double tap
*/
private static final int DOUBLE_TAP_SLOP = 100;
/**
- * Distance a touch needs to be outside of a window's bounds for it to
+ * Distance in dips a touch needs to be outside of a window's bounds for it to
* count as outside for purposes of dismissing the window.
*/
private static final int WINDOW_TOUCH_SLOP = 16;
/**
- * Minimum velocity to initiate a fling, as measured in pixels per second
+ * Minimum velocity to initiate a fling, as measured in dips per second
*/
private static final int MINIMUM_FLING_VELOCITY = 50;
/**
- * Maximum velocity to initiate a fling, as measured in pixels per second
+ * Maximum velocity to initiate a fling, as measured in dips per second
*/
private static final int MAXIMUM_FLING_VELOCITY = 8000;
/**
- * Distance between a touch up event denoting the end of a touch exploration
+ * Distance in dips between a touch up event denoting the end of a touch exploration
* gesture and the touch up event of a subsequent tap for the latter tap to be
* considered as a tap i.e. to perform a click.
*/
@@ -214,12 +220,12 @@ public class ViewConfiguration {
private static final float SCROLL_FRICTION = 0.015f;
/**
- * Max distance to overscroll for edge effects
+ * Max distance in dips to overscroll for edge effects
*/
private static final int OVERSCROLL_DISTANCE = 0;
/**
- * Max distance to overfling for edge effects
+ * Max distance in dips to overfling for edge effects
*/
private static final int OVERFLING_DISTANCE = 6;
@@ -229,6 +235,7 @@ public class ViewConfiguration {
private final int mMaximumFlingVelocity;
private final int mScrollbarSize;
private final int mTouchSlop;
+ private final int mDoubleTapTouchSlop;
private final int mPagingTouchSlop;
private final int mDoubleTapSlop;
private final int mScaledTouchExplorationTapSlop;
@@ -255,6 +262,7 @@ public class ViewConfiguration {
mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
mScrollbarSize = SCROLL_BAR_SIZE;
mTouchSlop = TOUCH_SLOP;
+ mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP;
mPagingTouchSlop = PAGING_TOUCH_SLOP;
mDoubleTapSlop = DOUBLE_TAP_SLOP;
mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP;
@@ -318,6 +326,8 @@ public class ViewConfiguration {
mTouchSlop = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
mPagingTouchSlop = mTouchSlop * 2;
+
+ mDoubleTapTouchSlop = mTouchSlop;
}
/**
@@ -342,7 +352,7 @@ public class ViewConfiguration {
/**
* @return The width of the horizontal scrollbar and the height of the vertical
- * scrollbar in pixels
+ * scrollbar in dips
*
* @deprecated Use {@link #getScaledScrollBarSize()} instead.
*/
@@ -374,7 +384,7 @@ public class ViewConfiguration {
}
/**
- * @return the length of the fading edges in pixels
+ * @return the length of the fading edges in dips
*
* @deprecated Use {@link #getScaledFadingEdgeLength()} instead.
*/
@@ -469,7 +479,7 @@ public class ViewConfiguration {
}
/**
- * @return Inset in pixels to look for touchable content when the user touches the edge of the
+ * @return Inset in dips to look for touchable content when the user touches the edge of the
* screen
*
* @deprecated Use {@link #getScaledEdgeSlop()} instead.
@@ -488,7 +498,7 @@ public class ViewConfiguration {
}
/**
- * @return Distance a touch can wander before we think the user is scrolling in pixels
+ * @return Distance in dips a touch can wander before we think the user is scrolling
*
* @deprecated Use {@link #getScaledTouchSlop()} instead.
*/
@@ -498,22 +508,31 @@ public class ViewConfiguration {
}
/**
- * @return Distance a touch can wander before we think the user is scrolling in pixels
+ * @return Distance in pixels a touch can wander before we think the user is scrolling
*/
public int getScaledTouchSlop() {
return mTouchSlop;
}
/**
- * @return Distance a touch can wander before we think the user is scrolling a full page
- * in dips
+ * @return Distance in pixels the first touch can wander before we do not consider this a
+ * potential double tap event
+ * @hide
+ */
+ public int getScaledDoubleTapTouchSlop() {
+ return mDoubleTapTouchSlop;
+ }
+
+ /**
+ * @return Distance in pixels a touch can wander before we think the user is scrolling a full
+ * page
*/
public int getScaledPagingTouchSlop() {
return mPagingTouchSlop;
}
/**
- * @return Distance between the first touch and second touch to still be
+ * @return Distance in dips between the first touch and second touch to still be
* considered a double tap
* @deprecated Use {@link #getScaledDoubleTapSlop()} instead.
* @hide The only client of this should be GestureDetector, which needs this
@@ -525,7 +544,7 @@ public class ViewConfiguration {
}
/**
- * @return Distance between the first touch and second touch to still be
+ * @return Distance in pixels between the first touch and second touch to still be
* considered a double tap
*/
public int getScaledDoubleTapSlop() {
@@ -533,7 +552,7 @@ public class ViewConfiguration {
}
/**
- * @return Distance between a touch up event denoting the end of a touch exploration
+ * @return Distance in pixels between a touch up event denoting the end of a touch exploration
* gesture and the touch up event of a subsequent tap for the latter tap to be
* considered as a tap i.e. to perform a click.
*
@@ -557,7 +576,7 @@ public class ViewConfiguration {
}
/**
- * @return Distance a touch must be outside the bounds of a window for it
+ * @return Distance in dips a touch must be outside the bounds of a window for it
* to be counted as outside the window for purposes of dismissing that
* window.
*
@@ -569,16 +588,15 @@ public class ViewConfiguration {
}
/**
- * @return Distance a touch must be outside the bounds of a window for it
- * to be counted as outside the window for purposes of dismissing that
- * window.
+ * @return Distance in pixels a touch must be outside the bounds of a window for it
+ * to be counted as outside the window for purposes of dismissing that window.
*/
public int getScaledWindowTouchSlop() {
return mWindowTouchSlop;
}
/**
- * @return Minimum velocity to initiate a fling, as measured in pixels per second.
+ * @return Minimum velocity to initiate a fling, as measured in dips per second.
*
* @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead.
*/
@@ -595,7 +613,7 @@ public class ViewConfiguration {
}
/**
- * @return Maximum velocity to initiate a fling, as measured in pixels per second.
+ * @return Maximum velocity to initiate a fling, as measured in dips per second.
*
* @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead.
*/
@@ -634,14 +652,16 @@ public class ViewConfiguration {
}
/**
- * @return The maximum distance a View should overscroll by when showing edge effects.
+ * @return The maximum distance a View should overscroll by when showing edge effects (in
+ * pixels).
*/
public int getScaledOverscrollDistance() {
return mOverscrollDistance;
}
/**
- * @return The maximum distance a View should overfling by when showing edge effects.
+ * @return The maximum distance a View should overfling by when showing edge effects (in
+ * pixels).
*/
public int getScaledOverflingDistance() {
return mOverflingDistance;
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
index 65e72c9..c1db572 100644
--- a/core/java/android/view/ViewDebug.java
+++ b/core/java/android/view/ViewDebug.java
@@ -127,16 +127,19 @@ public class ViewDebug {
* Logs the relative difference between the time an event was created and the time it
* was delivered.
*
- * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers().
- * This is time that the event loop spends blocked and unresponsive. Ideally, drawing
- * and animations should be perfectly synchronized with VSYNC so that swap buffers
- * is instantaneous.
+ * Logs the time spent waiting for Surface.lockCanvas(), Surface.unlockCanvasAndPost()
+ * or eglSwapBuffers(). This is time that the event loop spends blocked and unresponsive.
+ * Ideally, drawing and animations should be perfectly synchronized with VSYNC so that
+ * dequeuing and queueing buffers is instantaneous.
*
- * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw().
+ * Logs the time spent in ViewRoot.performTraversals() and ViewRoot.performDraw().
* @hide
*/
public static final boolean DEBUG_LATENCY = false;
+ /** @hide */
+ public static final String DEBUG_LATENCY_TAG = "ViewLatency";
+
/**
* <p>Enables or disables views consistency check. Even when this property is enabled,
* view consistency checks happen only if {@link false} is set
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index ee66c4d..d906a16 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -888,18 +888,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
/**
+ * Called when a view's visibility has changed. Notify the parent to take any appropriate
+ * action.
+ *
+ * @param child The view whose visibility has changed
+ * @param oldVisibility The previous visibility value (GONE, INVISIBLE, or VISIBLE).
+ * @param newVisibility The new visibility value (GONE, INVISIBLE, or VISIBLE).
* @hide
- * @param child
- * @param visibility
*/
- protected void onChildVisibilityChanged(View child, int visibility) {
+ protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
if (mTransition != null) {
- if (visibility == VISIBLE) {
- mTransition.showChild(this, child);
+ if (newVisibility == VISIBLE) {
+ mTransition.showChild(this, child, oldVisibility);
} else {
- mTransition.hideChild(this, child);
- }
- if (visibility != VISIBLE) {
+ mTransition.hideChild(this, child, newVisibility);
// Only track this on disappearing views - appearing views are already visible
// and don't need special handling during drawChild()
if (mVisibilityChangingChildren == null) {
@@ -914,7 +916,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
// in all cases, for drags
if (mCurrentDrag != null) {
- if (visibility == VISIBLE) {
+ if (newVisibility == VISIBLE) {
notifyChildOfDrag(child);
}
}
@@ -2229,6 +2231,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
@Override
void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
+ info.setClassName(ViewGroup.class.getName());
for (int i = 0, count = mChildrenCount; i < count; i++) {
View child = mChildren[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
@@ -2238,6 +2241,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
}
+ @Override
+ void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setClassName(ViewGroup.class.getName());
+ }
+
/**
* {@inheritDoc}
*/
@@ -2654,8 +2663,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
child.onAnimationStart();
}
- more = a.getTransformation(drawingTime, mChildTransformation,
- scalingRequired ? mAttachInfo.mApplicationScale : 1f);
+ more = a.getTransformation(drawingTime, mChildTransformation, 1f);
if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
if (mInvalidationTransformation == null) {
mInvalidationTransformation = new Transformation();
@@ -2952,6 +2960,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
mDrawLayers = enabled;
invalidate(true);
+ boolean flushLayers = !enabled;
+ AttachInfo info = mAttachInfo;
+ if (info != null && info.mHardwareRenderer != null &&
+ info.mHardwareRenderer.isEnabled()) {
+ if (!info.mHardwareRenderer.validate()) {
+ flushLayers = false;
+ }
+ } else {
+ flushLayers = false;
+ }
+
// We need to invalidate any child with a layer. For instance,
// if a child is backed by a hardware layer and we disable layers
// the child is marked as not dirty (flags cleared the last time
@@ -2962,6 +2981,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
for (int i = 0; i < mChildrenCount; i++) {
View child = mChildren[i];
if (child.mLayerType != LAYER_TYPE_NONE) {
+ if (flushLayers) child.flushLayer();
child.invalidate(true);
}
}
@@ -3044,8 +3064,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
/**
- * {@inheritDoc}
+ * Sets <code>t</code> to be the static transformation of the child, if set, returning a
+ * boolean to indicate whether a static transform was set. The default implementation
+ * simply returns <code>false</code>; subclasses may override this method for different
+ * behavior.
*
+ * @param child The child view whose static transform is being requested
+ * @param t The Transformation which will hold the result
+ * @return true if the transformation was set, false otherwise
* @see #setStaticTransformationsEnabled(boolean)
*/
protected boolean getChildStaticTransformation(View child, Transformation t) {
@@ -3943,8 +3969,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
} while (parent != null);
} else {
// Check whether the child that requests the invalidate is fully opaque
+ // Views being animated or transformed are not considered opaque because we may
+ // be invalidating their old position and need the parent to paint behind them.
+ Matrix childMatrix = child.getMatrix();
final boolean isOpaque = child.isOpaque() && !drawAnimation &&
- child.getAnimation() == null;
+ child.getAnimation() == null && childMatrix.isIdentity();
// Mark the child as dirty, using the appropriate flag
// Make sure we do not set both flags at the same time
int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY;
@@ -3958,7 +3987,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
- Matrix childMatrix = child.getMatrix();
if (!childMatrix.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java
index 84dc7d8..89a1ef2 100644
--- a/core/java/android/view/ViewPropertyAnimator.java
+++ b/core/java/android/view/ViewPropertyAnimator.java
@@ -837,6 +837,11 @@ public class ViewPropertyAnimator {
*/
@Override
public void onAnimationUpdate(ValueAnimator animation) {
+ PropertyBundle propertyBundle = mAnimatorMap.get(animation);
+ if (propertyBundle == null) {
+ // Shouldn't happen, but just to play it safe
+ return;
+ }
// alpha requires slightly different treatment than the other (transform) properties.
// The logic in setAlpha() is not simply setting mAlpha, plus the invalidation
// logic is dependent on how the view handles an internal call to onSetAlpha().
@@ -845,7 +850,6 @@ public class ViewPropertyAnimator {
boolean alphaHandled = false;
mView.invalidateParentCaches();
float fraction = animation.getAnimatedFraction();
- PropertyBundle propertyBundle = mAnimatorMap.get(animation);
int propertyMask = propertyBundle.mPropertyMask;
if ((propertyMask & TRANSFORM_MASK) != 0) {
mView.invalidate(false);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 6c982eb..1a4bdf4 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -57,7 +57,6 @@ import android.util.Poolable;
import android.util.PoolableManager;
import android.util.Pools;
import android.util.Slog;
-import android.util.SparseArray;
import android.util.TypedValue;
import android.view.View.MeasureSpec;
import android.view.accessibility.AccessibilityEvent;
@@ -65,6 +64,7 @@ import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import android.view.animation.AccelerateDecelerateInterpolator;
@@ -95,7 +95,8 @@ import java.util.List;
*/
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl extends Handler implements ViewParent,
- View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
+ View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks,
+ Choreographer.OnDrawListener {
private static final String TAG = "ViewRootImpl";
private static final boolean DBG = false;
private static final boolean LOCAL_LOGV = false;
@@ -109,7 +110,6 @@ public final class ViewRootImpl extends Handler implements ViewParent,
private static final boolean DEBUG_IMF = false || LOCAL_LOGV;
private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV;
private static final boolean DEBUG_FPS = false;
- private static final boolean WATCH_POINTER = false;
/**
* Set this system property to true to force the view hierarchy to render
@@ -153,9 +153,6 @@ public final class ViewRootImpl extends Handler implements ViewParent,
final TypedValue mTmpValue = new TypedValue();
final InputMethodCallback mInputMethodCallback;
- final SparseArray<Object> mPendingEvents = new SparseArray<Object>();
- int mPendingEventSeq = 0;
-
final Thread mThread;
final WindowLeaked mLocation;
@@ -203,13 +200,14 @@ public final class ViewRootImpl extends Handler implements ViewParent,
InputQueue.Callback mInputQueueCallback;
InputQueue mInputQueue;
FallbackEventHandler mFallbackEventHandler;
+ Choreographer mChoreographer;
final Rect mTempRect; // used in the transaction to not thrash the heap.
final Rect mVisRect; // used to retrieve visible rect of focused view.
boolean mTraversalScheduled;
long mLastTraversalFinishedTimeNanos;
- long mLastDrawDurationNanos;
+ long mLastDrawFinishedTimeNanos;
boolean mWillDrawSoon;
boolean mLayoutRequested;
boolean mFirst;
@@ -218,7 +216,16 @@ public final class ViewRootImpl extends Handler implements ViewParent,
boolean mNewSurfaceNeeded;
boolean mHasHadWindowFocus;
boolean mLastWasImTarget;
- InputEventMessage mPendingInputEvents = null;
+
+ // Pool of queued input events.
+ private static final int MAX_QUEUED_INPUT_EVENT_POOL_SIZE = 10;
+ private QueuedInputEvent mQueuedInputEventPool;
+ private int mQueuedInputEventPoolSize;
+
+ // Input event queue.
+ QueuedInputEvent mFirstPendingInputEvent;
+ QueuedInputEvent mCurrentInputEvent;
+ boolean mProcessInputEventsScheduled;
boolean mWindowAttributesChanged = false;
int mWindowAttributesChangesFlag = 0;
@@ -367,6 +374,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
mFallbackEventHandler = PolicyManager.makeNewFallbackEventHandler(context);
mProfileRendering = Boolean.parseBoolean(
SystemProperties.get(PROPERTY_PROFILE_RENDERING, "false"));
+ mChoreographer = Choreographer.getInstance();
}
public static void addFirstDrawHandler(Runnable callback) {
@@ -418,6 +426,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
+ mChoreographer.addOnDrawListener(this);
+
mView = view;
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs);
@@ -551,8 +561,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
mInputQueue = new InputQueue(mInputChannel);
mInputQueueCallback.onInputQueueCreated(mInputQueue);
} else {
- InputQueue.registerInputChannel(mInputChannel, mInputHandler,
- Looper.myQueue());
+ mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
+ Looper.myLooper());
}
}
@@ -787,23 +797,19 @@ public final class ViewRootImpl extends Handler implements ViewParent,
public void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
-
- //noinspection ConstantConditions
- if (ViewDebug.DEBUG_LATENCY && mLastTraversalFinishedTimeNanos != 0) {
- final long now = System.nanoTime();
- Log.d(TAG, "Latency: Scheduled traversal, it has been "
- + ((now - mLastTraversalFinishedTimeNanos) * 0.000001f)
- + "ms since the last traversal finished.");
- }
-
- sendEmptyMessage(DO_TRAVERSAL);
+ mChoreographer.scheduleDraw();
}
}
public void unscheduleTraversals() {
+ mTraversalScheduled = false;
+ }
+
+ @Override
+ public void onDraw() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
- removeMessages(DO_TRAVERSAL);
+ doTraversal();
}
}
@@ -840,24 +846,45 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- private void processInputEvents(boolean outOfOrder) {
- while (mPendingInputEvents != null) {
- handleMessage(mPendingInputEvents.mMessage);
- InputEventMessage tmpMessage = mPendingInputEvents;
- mPendingInputEvents = mPendingInputEvents.mNext;
- tmpMessage.recycle();
- if (outOfOrder) {
- removeMessages(PROCESS_INPUT_EVENTS);
+ private void doTraversal() {
+ doProcessInputEvents();
+
+ if (mProfile) {
+ Debug.startMethodTracing("ViewAncestor");
+ }
+
+ final long traversalStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ traversalStartTime = System.nanoTime();
+ if (mLastTraversalFinishedTimeNanos != 0) {
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals(); it has been "
+ + ((traversalStartTime - mLastTraversalFinishedTimeNanos) * 0.000001f)
+ + "ms since the last traversals finished.");
+ } else {
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting performTraversals().");
}
}
+
+ performTraversals();
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performTraversals() took "
+ + ((now - traversalStartTime) * 0.000001f)
+ + "ms.");
+ mLastTraversalFinishedTimeNanos = now;
+ }
+
+ if (mProfile) {
+ Debug.stopMethodTracing();
+ mProfile = false;
+ }
}
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
- processInputEvents(true);
-
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals");
@@ -867,10 +894,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (host == null || !mAdded)
return;
- mTraversalScheduled = false;
mWillDrawSoon = true;
boolean windowSizeMayChange = false;
- boolean fullRedrawNeeded = mFullRedrawNeeded;
boolean newSurface = false;
boolean surfaceChanged = false;
WindowManager.LayoutParams lp = mWindowAttributes;
@@ -895,7 +920,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
CompatibilityInfo compatibilityInfo = mCompatibilityInfo.get();
if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
params = lp;
- fullRedrawNeeded = true;
+ mFullRedrawNeeded = true;
mLayoutRequested = true;
if (mLastInCompatMode) {
params.flags &= ~WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
@@ -910,7 +935,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
Rect frame = mWinFrame;
if (mFirst) {
- fullRedrawNeeded = true;
+ mFullRedrawNeeded = true;
mLayoutRequested = true;
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {
@@ -954,7 +979,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(TAG,
"View " + host + " resized to: " + frame);
- fullRedrawNeeded = true;
+ mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
@@ -1292,7 +1317,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
// before actually drawing them, so it can display then
// all at once.
newSurface = true;
- fullRedrawNeeded = true;
+ mFullRedrawNeeded = true;
mPreviousTransparentRegion.setEmpty();
if (mAttachInfo.mHardwareRenderer != null) {
@@ -1328,7 +1353,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
} else if (surfaceGenerationId != mSurface.getGenerationId() &&
mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) {
- fullRedrawNeeded = true;
+ mFullRedrawNeeded = true;
try {
mAttachInfo.mHardwareRenderer.updateSurface(mHolder);
} catch (Surface.OutOfResourcesException e) {
@@ -1614,6 +1639,11 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
+ // Remember if we must report the next draw.
+ if ((relayoutResult & WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0) {
+ mReportNextDraw = true;
+ }
+
boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw() ||
viewVisibility != View.VISIBLE;
@@ -1624,42 +1654,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
mPendingTransitions.clear();
}
- mFullRedrawNeeded = false;
-
- final long drawStartTime;
- if (ViewDebug.DEBUG_LATENCY) {
- drawStartTime = System.nanoTime();
- }
-
- draw(fullRedrawNeeded);
-
- if (ViewDebug.DEBUG_LATENCY) {
- mLastDrawDurationNanos = System.nanoTime() - drawStartTime;
- }
- if ((relayoutResult&WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0
- || mReportNextDraw) {
- if (LOCAL_LOGV) {
- Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle());
- }
- mReportNextDraw = false;
- if (mSurfaceHolder != null && mSurface.isValid()) {
- mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder);
- SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
- if (callbacks != null) {
- for (SurfaceHolder.Callback c : callbacks) {
- if (c instanceof SurfaceHolder.Callback2) {
- ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(
- mSurfaceHolder);
- }
- }
- }
- }
- try {
- sWindowSession.finishDrawing(mWindow);
- } catch (RemoteException e) {
- }
- }
+ performDraw();
} else {
// End any pending transitions on this non-visible window
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
@@ -1668,14 +1664,6 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
mPendingTransitions.clear();
}
- // We were supposed to report when we are done drawing. Since we canceled the
- // draw, remember it here.
- if ((relayoutResult&WindowManagerImpl.RELAYOUT_RES_FIRST_TIME) != 0) {
- mReportNextDraw = true;
- }
- if (fullRedrawNeeded) {
- mFullRedrawNeeded = true;
- }
if (viewVisibility == View.VISIBLE) {
// Try again
@@ -1819,6 +1807,56 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
+ private void performDraw() {
+ final long drawStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ drawStartTime = System.nanoTime();
+ if (mLastDrawFinishedTimeNanos != 0) {
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting draw(); it has been "
+ + ((drawStartTime - mLastDrawFinishedTimeNanos) * 0.000001f)
+ + "ms since the last draw finished.");
+ } else {
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "Starting draw().");
+ }
+ }
+
+ final boolean fullRedrawNeeded = mFullRedrawNeeded;
+ mFullRedrawNeeded = false;
+ draw(fullRedrawNeeded);
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "performDraw() took "
+ + ((now - drawStartTime) * 0.000001f)
+ + "ms.");
+ mLastDrawFinishedTimeNanos = now;
+ }
+
+ if (mReportNextDraw) {
+ mReportNextDraw = false;
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle());
+ }
+ if (mSurfaceHolder != null && mSurface.isValid()) {
+ mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder);
+ SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ if (c instanceof SurfaceHolder.Callback2) {
+ ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(
+ mSurfaceHolder);
+ }
+ }
+ }
+ }
+ try {
+ sWindowSession.finishDrawing(mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (surface == null || !surface.isValid()) {
@@ -1857,8 +1895,9 @@ public final class ViewRootImpl extends Handler implements ViewParent,
mCurScrollY = yoff;
fullRedrawNeeded = true;
}
- float appScale = mAttachInfo.mApplicationScale;
- boolean scalingRequired = mAttachInfo.mScalingRequired;
+
+ final float appScale = mAttachInfo.mApplicationScale;
+ final boolean scalingRequired = mAttachInfo.mScalingRequired;
int resizeAlpha = 0;
if (mResizeBuffer != null) {
@@ -1873,7 +1912,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- Rect dirty = mDirty;
+ final Rect dirty = mDirty;
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
@@ -1891,35 +1930,6 @@ public final class ViewRootImpl extends Handler implements ViewParent,
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
- if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
- if (!dirty.isEmpty() || mIsAnimating) {
- mIsAnimating = false;
- mHardwareYOffset = yoff;
- mResizeAlpha = resizeAlpha;
-
- mCurrentDirty.set(dirty);
- mCurrentDirty.union(mPreviousDirty);
- mPreviousDirty.set(dirty);
- dirty.setEmpty();
-
- Rect currentDirty = mCurrentDirty;
- if (animating) {
- currentDirty = null;
- }
-
- if (mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this, currentDirty)) {
- mPreviousDirty.set(0, 0, mWidth, mHeight);
- }
- }
-
- if (animating) {
- mFullRedrawNeeded = true;
- scheduleTraversals();
- }
-
- return;
- }
-
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Draw " + mView + "/"
+ mWindowAttributes.getTitle()
@@ -1930,64 +1940,79 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
if (!dirty.isEmpty() || mIsAnimating) {
- Canvas canvas;
- try {
- int left = dirty.left;
- int top = dirty.top;
- int right = dirty.right;
- int bottom = dirty.bottom;
-
- final long lockCanvasStartTime;
- if (ViewDebug.DEBUG_LATENCY) {
- lockCanvasStartTime = System.nanoTime();
- }
+ if (mAttachInfo.mHardwareRenderer != null
+ && mAttachInfo.mHardwareRenderer.isEnabled()) {
+ // Draw with hardware renderer.
+ mIsAnimating = false;
+ mHardwareYOffset = yoff;
+ mResizeAlpha = resizeAlpha;
- canvas = surface.lockCanvas(dirty);
+ mCurrentDirty.set(dirty);
+ mCurrentDirty.union(mPreviousDirty);
+ mPreviousDirty.set(dirty);
+ dirty.setEmpty();
- if (ViewDebug.DEBUG_LATENCY) {
- long now = System.nanoTime();
- Log.d(TAG, "Latency: Spent "
- + ((now - lockCanvasStartTime) * 0.000001f)
- + "ms waiting for surface.lockCanvas()");
+ if (mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this,
+ animating ? null : mCurrentDirty)) {
+ mPreviousDirty.set(0, 0, mWidth, mHeight);
}
+ } else {
+ // Draw with software renderer.
+ Canvas canvas;
+ try {
+ int left = dirty.left;
+ int top = dirty.top;
+ int right = dirty.right;
+ int bottom = dirty.bottom;
+
+ final long lockCanvasStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ lockCanvasStartTime = System.nanoTime();
+ }
- if (left != dirty.left || top != dirty.top || right != dirty.right ||
- bottom != dirty.bottom) {
- mAttachInfo.mIgnoreDirtyState = true;
- }
+ canvas = mSurface.lockCanvas(dirty);
- // TODO: Do this in native
- canvas.setDensity(mDensity);
- } catch (Surface.OutOfResourcesException e) {
- Log.e(TAG, "OutOfResourcesException locking surface", e);
- try {
- if (!sWindowSession.outOfMemory(mWindow)) {
- Slog.w(TAG, "No processes killed for memory; killing self");
- Process.killProcess(Process.myPid());
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- lockCanvas() took "
+ + ((now - lockCanvasStartTime) * 0.000001f) + "ms");
}
- } catch (RemoteException ex) {
- }
- mLayoutRequested = true; // ask wm for a new surface next time.
- return;
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "IllegalArgumentException locking surface", e);
- // Don't assume this is due to out of memory, it could be
- // something else, and if it is something else then we could
- // kill stuff (or ourself) for no reason.
- mLayoutRequested = true; // ask wm for a new surface next time.
- return;
- }
- try {
- if (!dirty.isEmpty() || mIsAnimating) {
- long startTime = 0L;
+ if (left != dirty.left || top != dirty.top || right != dirty.right ||
+ bottom != dirty.bottom) {
+ mAttachInfo.mIgnoreDirtyState = true;
+ }
+
+ // TODO: Do this in native
+ canvas.setDensity(mDensity);
+ } catch (Surface.OutOfResourcesException e) {
+ Log.e(TAG, "OutOfResourcesException locking surface", e);
+ try {
+ if (!sWindowSession.outOfMemory(mWindow)) {
+ Slog.w(TAG, "No processes killed for memory; killing self");
+ Process.killProcess(Process.myPid());
+ }
+ } catch (RemoteException ex) {
+ }
+ mLayoutRequested = true; // ask wm for a new surface next time.
+ return;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "IllegalArgumentException locking surface", e);
+ // Don't assume this is due to out of memory, it could be
+ // something else, and if it is something else then we could
+ // kill stuff (or ourself) for no reason.
+ mLayoutRequested = true; // ask wm for a new surface next time.
+ return;
+ }
+ try {
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
+ long startTime = 0L;
if (ViewDebug.DEBUG_PROFILE_DRAWING) {
startTime = SystemClock.elapsedRealtime();
}
@@ -2023,7 +2048,19 @@ public final class ViewRootImpl extends Handler implements ViewParent,
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mAttachInfo.mSetIgnoreDirtyState = false;
+
+ final long drawStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ drawStartTime = System.nanoTime();
+ }
+
mView.draw(canvas);
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- draw() took "
+ + ((now - drawStartTime) * 0.000001f) + "ms");
+ }
} finally {
if (!mAttachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
@@ -2038,15 +2075,25 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (ViewDebug.DEBUG_PROFILE_DRAWING) {
EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
}
- }
+ } finally {
+ final long unlockCanvasAndPostStartTime;
+ if (ViewDebug.DEBUG_LATENCY) {
+ unlockCanvasAndPostStartTime = System.nanoTime();
+ }
- } finally {
- surface.unlockCanvasAndPost(canvas);
- }
- }
+ surface.unlockCanvasAndPost(canvas);
- if (LOCAL_LOGV) {
- Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
+ if (ViewDebug.DEBUG_LATENCY) {
+ long now = System.nanoTime();
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- unlockCanvasAndPost() took "
+ + ((now - unlockCanvasAndPostStartTime) * 0.000001f) + "ms");
+ }
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
+ }
+ }
+ }
}
if (animating) {
@@ -2265,8 +2312,9 @@ public final class ViewRootImpl extends Handler implements ViewParent,
mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
mInputQueueCallback = null;
mInputQueue = null;
- } else if (mInputChannel != null) {
- InputQueue.unregisterInputChannel(mInputChannel);
+ } else if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
}
try {
sWindowSession.remove(mWindow);
@@ -2279,6 +2327,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
mInputChannel.dispose();
mInputChannel = null;
}
+
+ mChoreographer.removeOnDrawListener(this);
}
void updateConfiguration(Configuration config, boolean force) {
@@ -2333,17 +2383,14 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- public final static int DO_TRAVERSAL = 1000;
public final static int DIE = 1001;
public final static int RESIZED = 1002;
public final static int RESIZED_REPORT = 1003;
public final static int WINDOW_FOCUS_CHANGED = 1004;
public final static int DISPATCH_KEY = 1005;
- public final static int DISPATCH_POINTER = 1006;
- public final static int DISPATCH_TRACKBALL = 1007;
public final static int DISPATCH_APP_VISIBILITY = 1008;
public final static int DISPATCH_GET_NEW_SURFACE = 1009;
- public final static int FINISHED_EVENT = 1010;
+ public final static int IME_FINISHED_EVENT = 1010;
public final static int DISPATCH_KEY_FROM_IME = 1011;
public final static int FINISH_INPUT_CONNECTION = 1012;
public final static int CHECK_FOCUS = 1013;
@@ -2356,14 +2403,12 @@ public final class ViewRootImpl extends Handler implements ViewParent,
public final static int DO_PERFORM_ACCESSIBILITY_ACTION = 1020;
public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 1021;
public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 1022;
- public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT = 1023;
- public final static int PROCESS_INPUT_EVENTS = 1024;
+ public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 1023;
+ public final static int DO_PROCESS_INPUT_EVENTS = 1024;
@Override
public String getMessageName(Message message) {
switch (message.what) {
- case DO_TRAVERSAL:
- return "DO_TRAVERSAL";
case DIE:
return "DIE";
case RESIZED:
@@ -2374,16 +2419,12 @@ public final class ViewRootImpl extends Handler implements ViewParent,
return "WINDOW_FOCUS_CHANGED";
case DISPATCH_KEY:
return "DISPATCH_KEY";
- case DISPATCH_POINTER:
- return "DISPATCH_POINTER";
- case DISPATCH_TRACKBALL:
- return "DISPATCH_TRACKBALL";
case DISPATCH_APP_VISIBILITY:
return "DISPATCH_APP_VISIBILITY";
case DISPATCH_GET_NEW_SURFACE:
return "DISPATCH_GET_NEW_SURFACE";
- case FINISHED_EVENT:
- return "FINISHED_EVENT";
+ case IME_FINISHED_EVENT:
+ return "IME_FINISHED_EVENT";
case DISPATCH_KEY_FROM_IME:
return "DISPATCH_KEY_FROM_IME";
case FINISH_INPUT_CONNECTION:
@@ -2408,11 +2449,10 @@ public final class ViewRootImpl extends Handler implements ViewParent,
return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID";
case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID:
return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID";
- case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT:
- return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT";
- case PROCESS_INPUT_EVENTS:
- return "PROCESS_INPUT_EVENTS";
-
+ case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT:
+ return "DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT";
+ case DO_PROCESS_INPUT_EVENTS:
+ return "DO_PROCESS_INPUT_EVENTS";
}
return super.getMessageName(message);
}
@@ -2428,51 +2468,12 @@ public final class ViewRootImpl extends Handler implements ViewParent,
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.release();
break;
- case DO_TRAVERSAL:
- if (mProfile) {
- Debug.startMethodTracing("ViewAncestor");
- }
-
- final long traversalStartTime;
- if (ViewDebug.DEBUG_LATENCY) {
- traversalStartTime = System.nanoTime();
- mLastDrawDurationNanos = 0;
- }
-
- performTraversals();
-
- if (ViewDebug.DEBUG_LATENCY) {
- long now = System.nanoTime();
- Log.d(TAG, "Latency: Spent "
- + ((now - traversalStartTime) * 0.000001f)
- + "ms in performTraversals(), with "
- + (mLastDrawDurationNanos * 0.000001f)
- + "ms of that time in draw()");
- mLastTraversalFinishedTimeNanos = now;
- }
-
- if (mProfile) {
- Debug.stopMethodTracing();
- mProfile = false;
- }
- break;
- case FINISHED_EVENT:
- handleFinishedEvent(msg.arg1, msg.arg2 != 0);
- break;
- case DISPATCH_KEY:
- deliverKeyEvent((KeyEvent)msg.obj, msg.arg1 != 0);
- break;
- case DISPATCH_POINTER:
- deliverPointerEvent((MotionEvent) msg.obj, msg.arg1 != 0);
+ case IME_FINISHED_EVENT:
+ handleImeFinishedEvent(msg.arg1, msg.arg2 != 0);
break;
- case DISPATCH_TRACKBALL:
- deliverTrackballEvent((MotionEvent) msg.obj, msg.arg1 != 0);
- break;
- case DISPATCH_GENERIC_MOTION:
- deliverGenericMotionEvent((MotionEvent) msg.obj, msg.arg1 != 0);
- break;
- case PROCESS_INPUT_EVENTS:
- processInputEvents(false);
+ case DO_PROCESS_INPUT_EVENTS:
+ mProcessInputEventsScheduled = false;
+ doProcessInputEvents();
break;
case DISPATCH_APP_VISIBILITY:
handleAppVisibility(msg.arg1 != 0);
@@ -2528,7 +2529,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
mFullRedrawNeeded = true;
try {
mAttachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight,
- mAttachInfo, mHolder);
+ mHolder);
} catch (Surface.OutOfResourcesException e) {
Log.e(TAG, "OutOfResourcesException locking surface", e);
try {
@@ -2583,6 +2584,10 @@ public final class ViewRootImpl extends Handler implements ViewParent,
case DIE:
doDie();
break;
+ case DISPATCH_KEY: {
+ KeyEvent event = (KeyEvent)msg.obj;
+ enqueueInputEvent(event, null, 0);
+ } break;
case DISPATCH_KEY_FROM_IME: {
if (LOCAL_LOGV) Log.v(
TAG, "Dispatching key "
@@ -2594,7 +2599,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
//noinspection UnusedAssignment
event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM);
}
- deliverKeyEventPostIme((KeyEvent)msg.obj, false);
+ enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME);
} break;
case FINISH_INPUT_CONNECTION: {
InputMethodManager imm = InputMethodManager.peekInstance();
@@ -2647,79 +2652,15 @@ public final class ViewRootImpl extends Handler implements ViewParent,
.findAccessibilityNodeInfoByViewIdUiThread(msg);
}
} break;
- case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: {
+ case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: {
if (mView != null) {
getAccessibilityInteractionController()
- .findAccessibilityNodeInfosByViewTextUiThread(msg);
+ .findAccessibilityNodeInfosByTextUiThread(msg);
}
} break;
}
}
- private void startInputEvent(InputQueue.FinishedCallback finishedCallback) {
- if (mFinishedCallback != null) {
- Slog.w(TAG, "Received a new input event from the input queue but there is "
- + "already an unfinished input event in progress.");
- }
-
- if (ViewDebug.DEBUG_LATENCY) {
- mInputEventReceiveTimeNanos = System.nanoTime();
- mInputEventDeliverTimeNanos = 0;
- mInputEventDeliverPostImeTimeNanos = 0;
- }
-
- mFinishedCallback = finishedCallback;
- }
-
- private void finishInputEvent(InputEvent event, boolean handled) {
- if (LOCAL_LOGV) Log.v(TAG, "Telling window manager input event is finished");
-
- if (mFinishedCallback == null) {
- Slog.w(TAG, "Attempted to tell the input queue that the current input event "
- + "is finished but there is no input event actually in progress.");
- return;
- }
-
- if (ViewDebug.DEBUG_LATENCY) {
- final long now = System.nanoTime();
- final long eventTime = event.getEventTimeNano();
- final StringBuilder msg = new StringBuilder();
- msg.append("Latency: Spent ");
- msg.append((now - mInputEventReceiveTimeNanos) * 0.000001f);
- msg.append("ms processing ");
- if (event instanceof KeyEvent) {
- final KeyEvent keyEvent = (KeyEvent)event;
- msg.append("key event, action=");
- msg.append(KeyEvent.actionToString(keyEvent.getAction()));
- } else {
- final MotionEvent motionEvent = (MotionEvent)event;
- msg.append("motion event, action=");
- msg.append(MotionEvent.actionToString(motionEvent.getAction()));
- msg.append(", historySize=");
- msg.append(motionEvent.getHistorySize());
- }
- msg.append(", handled=");
- msg.append(handled);
- msg.append(", received at +");
- msg.append((mInputEventReceiveTimeNanos - eventTime) * 0.000001f);
- if (mInputEventDeliverTimeNanos != 0) {
- msg.append("ms, delivered at +");
- msg.append((mInputEventDeliverTimeNanos - eventTime) * 0.000001f);
- }
- if (mInputEventDeliverPostImeTimeNanos != 0) {
- msg.append("ms, delivered post IME at +");
- msg.append((mInputEventDeliverPostImeTimeNanos - eventTime) * 0.000001f);
- }
- msg.append("ms, finished at +");
- msg.append((now - eventTime) * 0.000001f);
- msg.append("ms.");
- Log.d(TAG, msg.toString());
- }
-
- mFinishedCallback.finished(handled);
- mFinishedCallback = null;
- }
-
/**
* Something in the current window tells us we need to change the touch mode. For
* example, we are not in touch mode, and the user touches the screen.
@@ -2841,11 +2782,27 @@ public final class ViewRootImpl extends Handler implements ViewParent,
return false;
}
- private void deliverPointerEvent(MotionEvent event, boolean sendDone) {
+ private void deliverInputEvent(QueuedInputEvent q) {
if (ViewDebug.DEBUG_LATENCY) {
- mInputEventDeliverTimeNanos = System.nanoTime();
+ q.mDeliverTimeNanos = System.nanoTime();
}
+ if (q.mEvent instanceof KeyEvent) {
+ deliverKeyEvent(q);
+ } else {
+ final int source = q.mEvent.getSource();
+ if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ deliverPointerEvent(q);
+ } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ deliverTrackballEvent(q);
+ } else {
+ deliverGenericMotionEvent(q);
+ }
+ }
+ }
+
+ private void deliverPointerEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
final boolean isTouchEvent = event.isTouchEvent();
if (mInputEventConsistencyVerifier != null) {
if (isTouchEvent) {
@@ -2857,7 +2814,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
// If there is no view, then the event will not be handled.
if (mView == null || !mAdded) {
- finishMotionEvent(event, sendDone, false);
+ finishInputEvent(q, false);
return;
}
@@ -2892,41 +2849,23 @@ public final class ViewRootImpl extends Handler implements ViewParent,
lt.sample("B Dispatched PointerEvents ", System.nanoTime() - event.getEventTimeNano());
}
if (handled) {
- finishMotionEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
// Pointer event was unhandled.
- finishMotionEvent(event, sendDone, false);
+ finishInputEvent(q, false);
}
- private void finishMotionEvent(MotionEvent event, boolean sendDone, boolean handled) {
- event.recycle();
- if (sendDone) {
- finishInputEvent(event, handled);
- }
- //noinspection ConstantConditions
- if (LOCAL_LOGV || WATCH_POINTER) {
- if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
- Log.i(TAG, "Done dispatching!");
- }
- }
- }
-
- private void deliverTrackballEvent(MotionEvent event, boolean sendDone) {
- if (ViewDebug.DEBUG_LATENCY) {
- mInputEventDeliverTimeNanos = System.nanoTime();
- }
-
- if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event);
-
+ private void deliverTrackballEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTrackballEvent(event, 0);
}
// If there is no view, then the event will not be handled.
if (mView == null || !mAdded) {
- finishMotionEvent(event, sendDone, false);
+ finishInputEvent(q, false);
return;
}
@@ -2938,7 +2877,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
// touch mode here.
ensureTouchMode(false);
- finishMotionEvent(event, sendDone, true);
+ finishInputEvent(q, true);
mLastTrackballTime = Integer.MIN_VALUE;
return;
}
@@ -2962,18 +2901,18 @@ public final class ViewRootImpl extends Handler implements ViewParent,
case MotionEvent.ACTION_DOWN:
x.reset(2);
y.reset(2);
- deliverKeyEvent(new KeyEvent(curTime, curTime,
+ dispatchKey(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
- InputDevice.SOURCE_KEYBOARD), false);
+ InputDevice.SOURCE_KEYBOARD));
break;
case MotionEvent.ACTION_UP:
x.reset(2);
y.reset(2);
- deliverKeyEvent(new KeyEvent(curTime, curTime,
+ dispatchKey(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
- InputDevice.SOURCE_KEYBOARD), false);
+ InputDevice.SOURCE_KEYBOARD));
break;
}
@@ -3024,38 +2963,35 @@ public final class ViewRootImpl extends Handler implements ViewParent,
+ keycode);
movement--;
int repeatCount = accelMovement - movement;
- deliverKeyEvent(new KeyEvent(curTime, curTime,
+ dispatchKey(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_MULTIPLE, keycode, repeatCount, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
- InputDevice.SOURCE_KEYBOARD), false);
+ InputDevice.SOURCE_KEYBOARD));
}
while (movement > 0) {
if (DEBUG_TRACKBALL) Log.v("foo", "Delivering fake DPAD: "
+ keycode);
movement--;
curTime = SystemClock.uptimeMillis();
- deliverKeyEvent(new KeyEvent(curTime, curTime,
+ dispatchKey(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_DOWN, keycode, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
- InputDevice.SOURCE_KEYBOARD), false);
- deliverKeyEvent(new KeyEvent(curTime, curTime,
+ InputDevice.SOURCE_KEYBOARD));
+ dispatchKey(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_UP, keycode, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
- InputDevice.SOURCE_KEYBOARD), false);
- }
+ InputDevice.SOURCE_KEYBOARD));
+ }
mLastTrackballTime = curTime;
}
// Unfortunately we can't tell whether the application consumed the keys, so
// we always consider the trackball event handled.
- finishMotionEvent(event, sendDone, true);
+ finishInputEvent(q, true);
}
- private void deliverGenericMotionEvent(MotionEvent event, boolean sendDone) {
- if (ViewDebug.DEBUG_LATENCY) {
- mInputEventDeliverTimeNanos = System.nanoTime();
- }
-
+ private void deliverGenericMotionEvent(QueuedInputEvent q) {
+ final MotionEvent event = (MotionEvent)q.mEvent;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0);
}
@@ -3068,7 +3004,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (isJoystick) {
updateJoystickDirection(event, false);
}
- finishMotionEvent(event, sendDone, false);
+ finishInputEvent(q, false);
return;
}
@@ -3077,16 +3013,16 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (isJoystick) {
updateJoystickDirection(event, false);
}
- finishMotionEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
if (isJoystick) {
// Translate the joystick event into DPAD keys and try to deliver those.
updateJoystickDirection(event, true);
- finishMotionEvent(event, sendDone, true);
+ finishInputEvent(q, true);
} else {
- finishMotionEvent(event, sendDone, false);
+ finishInputEvent(q, false);
}
}
@@ -3108,9 +3044,9 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (xDirection != mLastJoystickXDirection) {
if (mLastJoystickXKeyCode != 0) {
- deliverKeyEvent(new KeyEvent(time, time,
+ dispatchKey(new KeyEvent(time, time,
KeyEvent.ACTION_UP, mLastJoystickXKeyCode, 0, metaState,
- deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false);
+ deviceId, 0, KeyEvent.FLAG_FALLBACK, source));
mLastJoystickXKeyCode = 0;
}
@@ -3119,17 +3055,17 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (xDirection != 0 && synthesizeNewKeys) {
mLastJoystickXKeyCode = xDirection > 0
? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT;
- deliverKeyEvent(new KeyEvent(time, time,
+ dispatchKey(new KeyEvent(time, time,
KeyEvent.ACTION_DOWN, mLastJoystickXKeyCode, 0, metaState,
- deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false);
+ deviceId, 0, KeyEvent.FLAG_FALLBACK, source));
}
}
if (yDirection != mLastJoystickYDirection) {
if (mLastJoystickYKeyCode != 0) {
- deliverKeyEvent(new KeyEvent(time, time,
+ dispatchKey(new KeyEvent(time, time,
KeyEvent.ACTION_UP, mLastJoystickYKeyCode, 0, metaState,
- deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false);
+ deviceId, 0, KeyEvent.FLAG_FALLBACK, source));
mLastJoystickYKeyCode = 0;
}
@@ -3138,9 +3074,9 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (yDirection != 0 && synthesizeNewKeys) {
mLastJoystickYKeyCode = yDirection > 0
? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP;
- deliverKeyEvent(new KeyEvent(time, time,
+ dispatchKey(new KeyEvent(time, time,
KeyEvent.ACTION_DOWN, mLastJoystickYKeyCode, 0, metaState,
- deviceId, 0, KeyEvent.FLAG_FALLBACK, source), false);
+ deviceId, 0, KeyEvent.FLAG_FALLBACK, source));
}
}
}
@@ -3231,91 +3167,81 @@ public final class ViewRootImpl extends Handler implements ViewParent,
return false;
}
- int enqueuePendingEvent(Object event, boolean sendDone) {
- int seq = mPendingEventSeq+1;
- if (seq < 0) seq = 0;
- mPendingEventSeq = seq;
- mPendingEvents.put(seq, event);
- return sendDone ? seq : -seq;
- }
-
- Object retrievePendingEvent(int seq) {
- if (seq < 0) seq = -seq;
- Object event = mPendingEvents.get(seq);
- if (event != null) {
- mPendingEvents.remove(seq);
- }
- return event;
- }
-
- private void deliverKeyEvent(KeyEvent event, boolean sendDone) {
- if (ViewDebug.DEBUG_LATENCY) {
- mInputEventDeliverTimeNanos = System.nanoTime();
- }
-
+ private void deliverKeyEvent(QueuedInputEvent q) {
+ final KeyEvent event = (KeyEvent)q.mEvent;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
- // If there is no view, then the event will not be handled.
- if (mView == null || !mAdded) {
- finishKeyEvent(event, sendDone, false);
- return;
- }
-
- if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView);
+ if ((q.mFlags & QueuedInputEvent.FLAG_DELIVER_POST_IME) == 0) {
+ // If there is no view, then the event will not be handled.
+ if (mView == null || !mAdded) {
+ finishInputEvent(q, false);
+ return;
+ }
- // Perform predispatching before the IME.
- if (mView.dispatchKeyEventPreIme(event)) {
- finishKeyEvent(event, sendDone, true);
- return;
- }
+ if (LOCAL_LOGV) Log.v(TAG, "Dispatching key " + event + " to " + mView);
- // Dispatch to the IME before propagating down the view hierarchy.
- // The IME will eventually call back into handleFinishedEvent.
- if (mLastWasImTarget) {
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- int seq = enqueuePendingEvent(event, sendDone);
- if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
- + seq + " event=" + event);
- imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback);
+ // Perform predispatching before the IME.
+ if (mView.dispatchKeyEventPreIme(event)) {
+ finishInputEvent(q, true);
return;
}
+
+ // Dispatch to the IME before propagating down the view hierarchy.
+ // The IME will eventually call back into handleImeFinishedEvent.
+ if (mLastWasImTarget) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ final int seq = event.getSequenceNumber();
+ if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
+ + seq + " event=" + event);
+ imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback);
+ return;
+ }
+ }
}
// Not dispatching to IME, continue with post IME actions.
- deliverKeyEventPostIme(event, sendDone);
+ deliverKeyEventPostIme(q);
}
- private void handleFinishedEvent(int seq, boolean handled) {
- final KeyEvent event = (KeyEvent)retrievePendingEvent(seq);
- if (DEBUG_IMF) Log.v(TAG, "IME finished event: seq=" + seq
- + " handled=" + handled + " event=" + event);
- if (event != null) {
- final boolean sendDone = seq >= 0;
+ void handleImeFinishedEvent(int seq, boolean handled) {
+ final QueuedInputEvent q = mCurrentInputEvent;
+ if (q != null && q.mEvent.getSequenceNumber() == seq) {
+ final KeyEvent event = (KeyEvent)q.mEvent;
+ if (DEBUG_IMF) {
+ Log.v(TAG, "IME finished event: seq=" + seq
+ + " handled=" + handled + " event=" + event);
+ }
if (handled) {
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
} else {
- deliverKeyEventPostIme(event, sendDone);
+ deliverKeyEventPostIme(q);
+ }
+ } else {
+ if (DEBUG_IMF) {
+ Log.v(TAG, "IME finished event: seq=" + seq
+ + " handled=" + handled + ", event not found!");
}
}
}
- private void deliverKeyEventPostIme(KeyEvent event, boolean sendDone) {
+ private void deliverKeyEventPostIme(QueuedInputEvent q) {
+ final KeyEvent event = (KeyEvent)q.mEvent;
if (ViewDebug.DEBUG_LATENCY) {
- mInputEventDeliverPostImeTimeNanos = System.nanoTime();
+ q.mDeliverPostImeTimeNanos = System.nanoTime();
}
// If the view went away, then the event will not be handled.
if (mView == null || !mAdded) {
- finishKeyEvent(event, sendDone, false);
+ finishInputEvent(q, false);
return;
}
// If the key's purpose is to exit touch mode then we consume it and consider it handled.
if (checkForLeavingTouchModeAndConsume(event)) {
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
@@ -3325,7 +3251,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
@@ -3335,14 +3261,14 @@ public final class ViewRootImpl extends Handler implements ViewParent,
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())) {
if (mView.dispatchKeyShortcutEvent(event)) {
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
}
// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
@@ -3397,14 +3323,14 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(
SoundEffectConstants.getContantForFocusDirection(direction));
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
- finishKeyEvent(event, sendDone, true);
+ finishInputEvent(q, true);
return;
}
}
@@ -3412,13 +3338,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
// Key was unhandled.
- finishKeyEvent(event, sendDone, false);
- }
-
- private void finishKeyEvent(KeyEvent event, boolean sendDone, boolean handled) {
- if (sendDone) {
- finishInputEvent(event, handled);
- }
+ finishInputEvent(q, false);
}
/* drag/drop */
@@ -3743,8 +3663,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- public void dispatchFinishedEvent(int seq, boolean handled) {
- Message msg = obtainMessage(FINISHED_EVENT);
+ void dispatchImeFinishedEvent(int seq, boolean handled) {
+ Message msg = obtainMessage(IME_FINISHED_EVENT);
msg.arg1 = seq;
msg.arg2 = handled ? 1 : 0;
sendMessage(msg);
@@ -3773,152 +3693,182 @@ public final class ViewRootImpl extends Handler implements ViewParent,
sendMessage(msg);
}
- private long mInputEventReceiveTimeNanos;
- private long mInputEventDeliverTimeNanos;
- private long mInputEventDeliverPostImeTimeNanos;
- private InputQueue.FinishedCallback mFinishedCallback;
-
- private final InputHandler mInputHandler = new InputHandler() {
- public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) {
- startInputEvent(finishedCallback);
- dispatchKey(event, true);
- }
-
- public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) {
- startInputEvent(finishedCallback);
- dispatchMotion(event, true);
- }
- };
-
/**
- * Utility class used to queue up input events which are then handled during
- * performTraversals(). Doing it this way allows us to ensure that we are up to date with
- * all input events just prior to drawing, instead of placing those events on the regular
- * handler queue, potentially behind a drawing event.
+ * Represents a pending input event that is waiting in a queue.
+ *
+ * Input events are processed in serial order by the timestamp specified by
+ * {@link InputEvent#getEventTime()}. In general, the input dispatcher delivers
+ * one input event to the application at a time and waits for the application
+ * to finish handling it before delivering the next one.
+ *
+ * However, because the application or IME can synthesize and inject multiple
+ * key events at a time without going through the input dispatcher, we end up
+ * needing a queue on the application's side.
*/
- static class InputEventMessage {
- Message mMessage;
- InputEventMessage mNext;
+ private static final class QueuedInputEvent {
+ public static final int FLAG_DELIVER_POST_IME = 1 << 0;
- private static final Object sPoolSync = new Object();
- private static InputEventMessage sPool;
- private static int sPoolSize = 0;
+ public QueuedInputEvent mNext;
- private static final int MAX_POOL_SIZE = 10;
+ public InputEvent mEvent;
+ public InputEventReceiver mReceiver;
+ public int mFlags;
- private InputEventMessage(Message m) {
- mMessage = m;
- mNext = null;
- }
+ // Used for latency calculations.
+ public long mReceiveTimeNanos;
+ public long mDeliverTimeNanos;
+ public long mDeliverPostImeTimeNanos;
+ }
- /**
- * Return a new Message instance from the global pool. Allows us to
- * avoid allocating new objects in many cases.
- */
- public static InputEventMessage obtain(Message msg) {
- synchronized (sPoolSync) {
- if (sPool != null) {
- InputEventMessage m = sPool;
- sPool = m.mNext;
- m.mNext = null;
- sPoolSize--;
- m.mMessage = msg;
- return m;
- }
- }
- return new InputEventMessage(msg);
+ private QueuedInputEvent obtainQueuedInputEvent(InputEvent event,
+ InputEventReceiver receiver, int flags) {
+ QueuedInputEvent q = mQueuedInputEventPool;
+ if (q != null) {
+ mQueuedInputEventPoolSize -= 1;
+ mQueuedInputEventPool = q.mNext;
+ q.mNext = null;
+ } else {
+ q = new QueuedInputEvent();
}
- /**
- * Return the message to the pool.
- */
- public void recycle() {
- mMessage.recycle();
- synchronized (sPoolSync) {
- if (sPoolSize < MAX_POOL_SIZE) {
- mNext = sPool;
- sPool = this;
- sPoolSize++;
- }
- }
+ q.mEvent = event;
+ q.mReceiver = receiver;
+ q.mFlags = flags;
+ return q;
+ }
+ private void recycleQueuedInputEvent(QueuedInputEvent q) {
+ q.mEvent = null;
+ q.mReceiver = null;
+
+ if (mQueuedInputEventPoolSize < MAX_QUEUED_INPUT_EVENT_POOL_SIZE) {
+ mQueuedInputEventPoolSize += 1;
+ q.mNext = mQueuedInputEventPool;
+ mQueuedInputEventPool = q;
}
}
- /**
- * Place the input event message at the end of the current pending list
- */
- private void enqueueInputEvent(Message msg, long when) {
- InputEventMessage inputMessage = InputEventMessage.obtain(msg);
- if (mPendingInputEvents == null) {
- mPendingInputEvents = inputMessage;
+ void enqueueInputEvent(InputEvent event,
+ InputEventReceiver receiver, int flags) {
+ QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
+
+ if (ViewDebug.DEBUG_LATENCY) {
+ q.mReceiveTimeNanos = System.nanoTime();
+ q.mDeliverTimeNanos = 0;
+ q.mDeliverPostImeTimeNanos = 0;
+ }
+
+ // Always enqueue the input event in order, regardless of its time stamp.
+ // We do this because the application or the IME may inject key events
+ // in response to touch events and we want to ensure that the injected keys
+ // are processed in the order they were received and we cannot trust that
+ // the time stamp of injected events are monotonic.
+ QueuedInputEvent last = mFirstPendingInputEvent;
+ if (last == null) {
+ mFirstPendingInputEvent = q;
} else {
- InputEventMessage currMessage = mPendingInputEvents;
- while (currMessage.mNext != null) {
- currMessage = currMessage.mNext;
+ while (last.mNext != null) {
+ last = last.mNext;
}
- currMessage.mNext = inputMessage;
+ last.mNext = q;
}
- sendEmptyMessageAtTime(PROCESS_INPUT_EVENTS, when);
+
+ scheduleProcessInputEvents();
}
- public void dispatchKey(KeyEvent event) {
- dispatchKey(event, false);
+ private void scheduleProcessInputEvents() {
+ if (!mProcessInputEventsScheduled) {
+ mProcessInputEventsScheduled = true;
+ sendEmptyMessage(DO_PROCESS_INPUT_EVENTS);
+ }
}
- private void dispatchKey(KeyEvent event, boolean sendDone) {
- //noinspection ConstantConditions
- if (false && event.getAction() == KeyEvent.ACTION_DOWN) {
- if (event.getKeyCode() == KeyEvent.KEYCODE_CAMERA) {
- if (DBG) Log.d("keydisp", "===================================================");
- if (DBG) Log.d("keydisp", "Focused view Hierarchy is:");
+ private void doProcessInputEvents() {
+ while (mCurrentInputEvent == null && mFirstPendingInputEvent != null) {
+ QueuedInputEvent q = mFirstPendingInputEvent;
+ mFirstPendingInputEvent = q.mNext;
+ q.mNext = null;
+ mCurrentInputEvent = q;
+ deliverInputEvent(q);
+ }
+
+ // We are done processing all input events that we can process right now
+ // so we can clear the pending flag immediately.
+ if (mProcessInputEventsScheduled) {
+ mProcessInputEventsScheduled = false;
+ removeMessages(DO_PROCESS_INPUT_EVENTS);
+ }
+ }
- debug();
+ private void finishInputEvent(QueuedInputEvent q, boolean handled) {
+ if (q != mCurrentInputEvent) {
+ throw new IllegalStateException("finished input event out of order");
+ }
- if (DBG) Log.d("keydisp", "===================================================");
+ if (ViewDebug.DEBUG_LATENCY) {
+ final long now = System.nanoTime();
+ final long eventTime = q.mEvent.getEventTimeNano();
+ final StringBuilder msg = new StringBuilder();
+ msg.append("Spent ");
+ msg.append((now - q.mReceiveTimeNanos) * 0.000001f);
+ msg.append("ms processing ");
+ if (q.mEvent instanceof KeyEvent) {
+ final KeyEvent keyEvent = (KeyEvent)q.mEvent;
+ msg.append("key event, action=");
+ msg.append(KeyEvent.actionToString(keyEvent.getAction()));
+ } else {
+ final MotionEvent motionEvent = (MotionEvent)q.mEvent;
+ msg.append("motion event, action=");
+ msg.append(MotionEvent.actionToString(motionEvent.getAction()));
+ msg.append(", historySize=");
+ msg.append(motionEvent.getHistorySize());
}
+ msg.append(", handled=");
+ msg.append(handled);
+ msg.append(", received at +");
+ msg.append((q.mReceiveTimeNanos - eventTime) * 0.000001f);
+ if (q.mDeliverTimeNanos != 0) {
+ msg.append("ms, delivered at +");
+ msg.append((q.mDeliverTimeNanos - eventTime) * 0.000001f);
+ }
+ if (q.mDeliverPostImeTimeNanos != 0) {
+ msg.append("ms, delivered post IME at +");
+ msg.append((q.mDeliverPostImeTimeNanos - eventTime) * 0.000001f);
+ }
+ msg.append("ms, finished at +");
+ msg.append((now - eventTime) * 0.000001f);
+ msg.append("ms.");
+ Log.d(ViewDebug.DEBUG_LATENCY_TAG, msg.toString());
}
- Message msg = obtainMessage(DISPATCH_KEY);
- msg.obj = event;
- msg.arg1 = sendDone ? 1 : 0;
+ if (q.mReceiver != null) {
+ q.mReceiver.finishInputEvent(q.mEvent, handled);
+ } else {
+ q.mEvent.recycleIfNeededAfterDispatch();
+ }
- if (LOCAL_LOGV) Log.v(
- TAG, "sending key " + event + " to " + mView);
+ recycleQueuedInputEvent(q);
- enqueueInputEvent(msg, event.getEventTime());
- }
-
- private void dispatchMotion(MotionEvent event, boolean sendDone) {
- int source = event.getSource();
- if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
- dispatchPointer(event, sendDone);
- } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
- dispatchTrackball(event, sendDone);
- } else {
- dispatchGenericMotion(event, sendDone);
+ mCurrentInputEvent = null;
+ if (mFirstPendingInputEvent != null) {
+ scheduleProcessInputEvents();
}
}
- private void dispatchPointer(MotionEvent event, boolean sendDone) {
- Message msg = obtainMessage(DISPATCH_POINTER);
- msg.obj = event;
- msg.arg1 = sendDone ? 1 : 0;
- enqueueInputEvent(msg, event.getEventTime());
- }
+ final class WindowInputEventReceiver extends InputEventReceiver {
+ public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
- private void dispatchTrackball(MotionEvent event, boolean sendDone) {
- Message msg = obtainMessage(DISPATCH_TRACKBALL);
- msg.obj = event;
- msg.arg1 = sendDone ? 1 : 0;
- enqueueInputEvent(msg, event.getEventTime());
+ @Override
+ public void onInputEvent(InputEvent event) {
+ enqueueInputEvent(event, this, 0);
+ }
}
+ WindowInputEventReceiver mInputEventReceiver;
- private void dispatchGenericMotion(MotionEvent event, boolean sendDone) {
- Message msg = obtainMessage(DISPATCH_GENERIC_MOTION);
- msg.obj = event;
- msg.arg1 = sendDone ? 1 : 0;
- enqueueInputEvent(msg, event.getEventTime());
+ public void dispatchKey(KeyEvent event) {
+ enqueueInputEvent(event, null, 0);
}
public void dispatchAppVisibility(boolean visible) {
@@ -4100,7 +4050,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
public void finishedEvent(int seq, boolean handled) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
- viewAncestor.dispatchFinishedEvent(seq, handled);
+ viewAncestor.dispatchImeFinishedEvent(seq, handled);
}
}
@@ -4575,52 +4525,52 @@ public final class ViewRootImpl extends Handler implements ViewParent,
*/
static final class AccessibilityInteractionConnection
extends IAccessibilityInteractionConnection.Stub {
- private final WeakReference<ViewRootImpl> mRootImpl;
+ private final WeakReference<ViewRootImpl> mViewRootImpl;
- AccessibilityInteractionConnection(ViewRootImpl viewAncestor) {
- mRootImpl = new WeakReference<ViewRootImpl>(viewAncestor);
+ AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {
+ mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);
}
- public void findAccessibilityNodeInfoByAccessibilityId(int accessibilityId,
+ public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid) {
- ViewRootImpl viewRootImpl = mRootImpl.get();
- if (viewRootImpl != null) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
- .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityId,
+ .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
interactionId, callback, interrogatingPid, interrogatingTid);
}
}
- public void performAccessibilityAction(int accessibilityId, int action,
+ public void performAccessibilityAction(long accessibilityNodeId, int action,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interogatingPid, long interrogatingTid) {
- ViewRootImpl viewRootImpl = mRootImpl.get();
- if (viewRootImpl != null) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
- .performAccessibilityActionClientThread(accessibilityId, action, interactionId,
- callback, interogatingPid, interrogatingTid);
+ .performAccessibilityActionClientThread(accessibilityNodeId, action,
+ interactionId, callback, interogatingPid, interrogatingTid);
}
}
public void findAccessibilityNodeInfoByViewId(int viewId,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid) {
- ViewRootImpl viewRootImpl = mRootImpl.get();
- if (viewRootImpl != null) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
.findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback,
interrogatingPid, interrogatingTid);
}
}
- public void findAccessibilityNodeInfosByViewText(String text, int accessibilityId,
+ public void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid) {
- ViewRootImpl viewRootImpl = mRootImpl.get();
- if (viewRootImpl != null) {
+ ViewRootImpl viewRootImpl = mViewRootImpl.get();
+ if (viewRootImpl != null && viewRootImpl.mView != null) {
viewRootImpl.getAccessibilityInteractionController()
- .findAccessibilityNodeInfosByViewTextClientThread(text, accessibilityId,
+ .findAccessibilityNodeInfosByTextClientThread(text, accessibilityNodeId,
interactionId, callback, interrogatingPid, interrogatingTid);
}
}
@@ -4693,14 +4643,18 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- public void findAccessibilityNodeInfoByAccessibilityIdClientThread(int accessibilityId,
- int interactionId, IAccessibilityInteractionConnectionCallback callback,
- int interrogatingPid, long interrogatingTid) {
+ public void findAccessibilityNodeInfoByAccessibilityIdClientThread(
+ long accessibilityNodeId, int interactionId,
+ IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
+ long interrogatingTid) {
Message message = Message.obtain();
message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;
- message.arg1 = accessibilityId;
- message.arg2 = interactionId;
- message.obj = callback;
+ SomeArgs args = mPool.acquire();
+ args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.argi3 = interactionId;
+ args.arg1 = callback;
+ message.obj = args;
// If the interrogation is performed by the same thread as the main UI
// thread in this process, set the message as a static reference so
// after this call completes the same thread but in the interrogating
@@ -4708,23 +4662,31 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (interrogatingPid == Process.myPid()
&& interrogatingTid == Looper.getMainLooper().getThread().getId()) {
message.setTarget(ViewRootImpl.this);
- AccessibilityInteractionClient.getInstance().setSameThreadMessage(message);
+ AccessibilityInteractionClient.getInstanceForThread(
+ interrogatingTid).setSameThreadMessage(message);
} else {
sendMessage(message);
}
}
public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
- final int accessibilityId = message.arg1;
- final int interactionId = message.arg2;
+ SomeArgs args = (SomeArgs) message.obj;
+ final int accessibilityViewId = args.argi1;
+ final int virtualDescendantId = args.argi2;
+ final int interactionId = args.argi3;
final IAccessibilityInteractionConnectionCallback callback =
- (IAccessibilityInteractionConnectionCallback) message.obj;
-
+ (IAccessibilityInteractionConnectionCallback) args.arg1;
+ mPool.release(args);
AccessibilityNodeInfo info = null;
try {
- View target = findViewByAccessibilityId(accessibilityId);
- if (target != null) {
- info = target.createAccessibilityNodeInfo();
+ View target = findViewByAccessibilityId(accessibilityViewId);
+ if (target != null && target.getVisibility() == View.VISIBLE) {
+ AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
+ if (provider != null) {
+ info = provider.createAccessibilityNodeInfo(virtualDescendantId);
+ } else if (virtualDescendantId == View.NO_ID) {
+ info = target.createAccessibilityNodeInfo();
+ }
}
} finally {
try {
@@ -4750,7 +4712,8 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (interrogatingPid == Process.myPid()
&& interrogatingTid == Looper.getMainLooper().getThread().getId()) {
message.setTarget(ViewRootImpl.this);
- AccessibilityInteractionClient.getInstance().setSameThreadMessage(message);
+ AccessibilityInteractionClient.getInstanceForThread(
+ interrogatingTid).setSameThreadMessage(message);
} else {
sendMessage(message);
}
@@ -4778,16 +4741,17 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- public void findAccessibilityNodeInfosByViewTextClientThread(String text,
- int accessibilityViewId, int interactionId,
+ public void findAccessibilityNodeInfosByTextClientThread(String text,
+ long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
long interrogatingTid) {
Message message = Message.obtain();
- message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT;
+ message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT;
SomeArgs args = mPool.acquire();
args.arg1 = text;
- args.argi1 = accessibilityViewId;
- args.argi2 = interactionId;
+ args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
+ args.argi3 = interactionId;
args.arg2 = callback;
message.obj = args;
// If the interrogation is performed by the same thread as the main UI
@@ -4797,53 +4761,64 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (interrogatingPid == Process.myPid()
&& interrogatingTid == Looper.getMainLooper().getThread().getId()) {
message.setTarget(ViewRootImpl.this);
- AccessibilityInteractionClient.getInstance().setSameThreadMessage(message);
+ AccessibilityInteractionClient.getInstanceForThread(
+ interrogatingTid).setSameThreadMessage(message);
} else {
sendMessage(message);
}
}
- public void findAccessibilityNodeInfosByViewTextUiThread(Message message) {
+ public void findAccessibilityNodeInfosByTextUiThread(Message message) {
SomeArgs args = (SomeArgs) message.obj;
final String text = (String) args.arg1;
final int accessibilityViewId = args.argi1;
- final int interactionId = args.argi2;
+ final int virtualDescendantId = args.argi2;
+ final int interactionId = args.argi3;
final IAccessibilityInteractionConnectionCallback callback =
(IAccessibilityInteractionConnectionCallback) args.arg2;
mPool.release(args);
-
List<AccessibilityNodeInfo> infos = null;
try {
- ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList;
- foundViews.clear();
-
- View root = null;
+ View target = null;
if (accessibilityViewId != View.NO_ID) {
- root = findViewByAccessibilityId(accessibilityViewId);
+ target = findViewByAccessibilityId(accessibilityViewId);
} else {
- root = ViewRootImpl.this.mView;
- }
-
- if (root == null || root.getVisibility() != View.VISIBLE) {
- return;
- }
-
- root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT
- | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
- if (foundViews.isEmpty()) {
- return;
+ target = ViewRootImpl.this.mView;
}
-
- infos = mTempAccessibilityNodeInfoList;
- infos.clear();
-
- final int viewCount = foundViews.size();
- for (int i = 0; i < viewCount; i++) {
- View foundView = foundViews.get(i);
- if (foundView.getVisibility() == View.VISIBLE) {
- infos.add(foundView.createAccessibilityNodeInfo());
+ if (target != null && target.getVisibility() == View.VISIBLE) {
+ AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
+ if (provider != null) {
+ infos = provider.findAccessibilityNodeInfosByText(text,
+ virtualDescendantId);
+ } else if (virtualDescendantId == View.NO_ID) {
+ ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList;
+ foundViews.clear();
+ target.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT
+ | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION
+ | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS);
+ if (!foundViews.isEmpty()) {
+ infos = mTempAccessibilityNodeInfoList;
+ infos.clear();
+ final int viewCount = foundViews.size();
+ for (int i = 0; i < viewCount; i++) {
+ View foundView = foundViews.get(i);
+ if (foundView.getVisibility() == View.VISIBLE) {
+ provider = foundView.getAccessibilityNodeProvider();
+ if (provider != null) {
+ List<AccessibilityNodeInfo> infosFromProvider =
+ provider.findAccessibilityNodeInfosByText(text,
+ virtualDescendantId);
+ if (infosFromProvider != null) {
+ infos.addAll(infosFromProvider);
+ }
+ } else {
+ infos.add(foundView.createAccessibilityNodeInfo());
+ }
+ }
+ }
+ }
}
- }
+ }
} finally {
try {
callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
@@ -4853,15 +4828,16 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- public void performAccessibilityActionClientThread(int accessibilityId, int action,
+ public void performAccessibilityActionClientThread(long accessibilityNodeId, int action,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interogatingPid, long interrogatingTid) {
Message message = Message.obtain();
message.what = DO_PERFORM_ACCESSIBILITY_ACTION;
+ message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
+ message.arg2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
SomeArgs args = mPool.acquire();
- args.argi1 = accessibilityId;
- args.argi2 = action;
- args.argi3 = interactionId;
+ args.argi1 = action;
+ args.argi2 = interactionId;
args.arg1 = callback;
message.obj = args;
// If the interrogation is performed by the same thread as the main UI
@@ -4871,36 +4847,60 @@ public final class ViewRootImpl extends Handler implements ViewParent,
if (interogatingPid == Process.myPid()
&& interrogatingTid == Looper.getMainLooper().getThread().getId()) {
message.setTarget(ViewRootImpl.this);
- AccessibilityInteractionClient.getInstance().setSameThreadMessage(message);
+ AccessibilityInteractionClient.getInstanceForThread(
+ interrogatingTid).setSameThreadMessage(message);
} else {
sendMessage(message);
}
}
public void perfromAccessibilityActionUiThread(Message message) {
+ final int accessibilityViewId = message.arg1;
+ final int virtualDescendantId = message.arg2;
SomeArgs args = (SomeArgs) message.obj;
- final int accessibilityId = args.argi1;
- final int action = args.argi2;
- final int interactionId = args.argi3;
+ final int action = args.argi1;
+ final int interactionId = args.argi2;
final IAccessibilityInteractionConnectionCallback callback =
(IAccessibilityInteractionConnectionCallback) args.arg1;
mPool.release(args);
-
boolean succeeded = false;
try {
- switch (action) {
- case AccessibilityNodeInfo.ACTION_FOCUS: {
- succeeded = performActionFocus(accessibilityId);
- } break;
- case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
- succeeded = performActionClearFocus(accessibilityId);
- } break;
- case AccessibilityNodeInfo.ACTION_SELECT: {
- succeeded = performActionSelect(accessibilityId);
- } break;
- case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: {
- succeeded = performActionClearSelection(accessibilityId);
- } break;
+ View target = findViewByAccessibilityId(accessibilityViewId);
+ if (target != null && target.getVisibility() == View.VISIBLE) {
+ AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
+ if (provider != null) {
+ succeeded = provider.performAccessibilityAction(action,
+ virtualDescendantId);
+ } else if (virtualDescendantId == View.NO_ID) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_FOCUS: {
+ if (!target.hasFocus()) {
+ // Get out of touch mode since accessibility
+ // wants to move focus around.
+ ensureTouchMode(false);
+ succeeded = target.requestFocus();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
+ if (target.hasFocus()) {
+ target.clearFocus();
+ succeeded = !target.isFocused();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_SELECT: {
+ if (!target.isSelected()) {
+ target.setSelected(true);
+ succeeded = target.isSelected();
+ }
+ } break;
+ case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: {
+ if (target.isSelected()) {
+ target.setSelected(false);
+ succeeded = !target.isSelected();
+ }
+ } break;
+ }
+ }
}
} finally {
try {
@@ -4911,52 +4911,6 @@ public final class ViewRootImpl extends Handler implements ViewParent,
}
}
- private boolean performActionFocus(int accessibilityId) {
- View target = findViewByAccessibilityId(accessibilityId);
- if (target == null || target.getVisibility() != View.VISIBLE) {
- return false;
- }
- // Get out of touch mode since accessibility wants to move focus around.
- ensureTouchMode(false);
- return target.requestFocus();
- }
-
- private boolean performActionClearFocus(int accessibilityId) {
- View target = findViewByAccessibilityId(accessibilityId);
- if (target == null || target.getVisibility() != View.VISIBLE) {
- return false;
- }
- if (!target.isFocused()) {
- return false;
- }
- target.clearFocus();
- return !target.isFocused();
- }
-
- private boolean performActionSelect(int accessibilityId) {
- View target = findViewByAccessibilityId(accessibilityId);
- if (target == null || target.getVisibility() != View.VISIBLE) {
- return false;
- }
- if (target.isSelected()) {
- return false;
- }
- target.setSelected(true);
- return target.isSelected();
- }
-
- private boolean performActionClearSelection(int accessibilityId) {
- View target = findViewByAccessibilityId(accessibilityId);
- if (target == null || target.getVisibility() != View.VISIBLE) {
- return false;
- }
- if (!target.isSelected()) {
- return false;
- }
- target.setSelected(false);
- return !target.isSelected();
- }
-
private View findViewByAccessibilityId(int accessibilityId) {
View root = ViewRootImpl.this.mView;
if (root == null) {
diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java
index db87175..7fd3389 100644
--- a/core/java/android/view/ViewTreeObserver.java
+++ b/core/java/android/view/ViewTreeObserver.java
@@ -185,7 +185,8 @@ public final class ViewTreeObserver {
mTouchableInsets = TOUCHABLE_INSETS_FRAME;
}
- @Override public boolean equals(Object o) {
+ @Override
+ public boolean equals(Object o) {
try {
if (o == null) {
return false;
@@ -288,6 +289,14 @@ public final class ViewTreeObserver {
}
}
+ if (observer.mOnScrollChangedListeners != null) {
+ if (mOnScrollChangedListeners != null) {
+ mOnScrollChangedListeners.addAll(observer.mOnScrollChangedListeners);
+ } else {
+ mOnScrollChangedListeners = observer.mOnScrollChangedListeners;
+ }
+ }
+
observer.kill();
}
@@ -349,10 +358,26 @@ public final class ViewTreeObserver {
* @param victim The callback to remove
*
* @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @deprecated Use #removeOnGlobalLayoutListener instead
*
* @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
*/
+ @Deprecated
public void removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) {
+ removeOnGlobalLayoutListener(victim);
+ }
+
+ /**
+ * Remove a previously installed global layout callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
+ */
+ public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
return;
diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java
index 48fe0df..24a3066 100644
--- a/core/java/android/view/VolumePanel.java
+++ b/core/java/android/view/VolumePanel.java
@@ -400,7 +400,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")");
if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
- if (mActiveStreamType == -1) {
+ if (mActiveStreamType != streamType) {
reorderSliders(streamType);
}
onShowVolumeChanged(streamType, flags);
diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java
index cbaa3df..6ec2e8d 100644
--- a/core/java/android/view/WindowManagerPolicy.java
+++ b/core/java/android/view/WindowManagerPolicy.java
@@ -353,7 +353,8 @@ public interface WindowManagerPolicy {
* Add a fake window to the window manager. This window sits
* at the top of the other windows and consumes events.
*/
- public FakeWindow addFakeWindow(Looper looper, InputHandler inputHandler,
+ public FakeWindow addFakeWindow(Looper looper,
+ InputEventReceiver.Factory inputEventReceiverFactory,
String name, int windowType, int layoutParamsFlags, boolean canReceiveKeys,
boolean hasFocus, boolean touchFullscreen);
}
diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java
index b46028e..c28b220 100755
--- a/core/java/android/view/WindowOrientationListener.java
+++ b/core/java/android/view/WindowOrientationListener.java
@@ -21,6 +21,7 @@ import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.util.FloatMath;
import android.util.Log;
import android.util.Slog;
@@ -48,6 +49,8 @@ public abstract class WindowOrientationListener {
private static final boolean DEBUG = false;
private static final boolean localLOGV = DEBUG || false;
+ private static final boolean USE_GRAVITY_SENSOR = false;
+
private SensorManager mSensorManager;
private boolean mEnabled;
private int mRate;
@@ -79,7 +82,8 @@ public abstract class WindowOrientationListener {
private WindowOrientationListener(Context context, int rate) {
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
mRate = rate;
- mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ mSensor = mSensorManager.getDefaultSensor(USE_GRAVITY_SENSOR
+ ? Sensor.TYPE_GRAVITY : Sensor.TYPE_ACCELEROMETER);
if (mSensor != null) {
// Create listener only if sensors do exist
mSensorEventListener = new SensorEventListenerImpl(this);
@@ -179,7 +183,7 @@ public abstract class WindowOrientationListener {
* cartesian space because the orientation calculations are sensitive to the
* absolute magnitude of the acceleration. In particular, there are singularities
* in the calculation as the magnitude approaches 0. By performing the low-pass
- * filtering early, we can eliminate high-frequency impulses systematically.
+ * filtering early, we can eliminate most spurious high-frequency impulses due to noise.
*
* - Convert the acceleromter vector from cartesian to spherical coordinates.
* Since we're dealing with rotation of the device, this is the sensible coordinate
@@ -204,11 +208,17 @@ public abstract class WindowOrientationListener {
* new orientation proposal.
*
* Details are explained inline.
+ *
+ * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for
+ * signal processing background.
*/
static final class SensorEventListenerImpl implements SensorEventListener {
// We work with all angles in degrees in this class.
private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI);
+ // Number of nanoseconds per millisecond.
+ private static final long NANOS_PER_MS = 1000000;
+
// Indices into SensorEvent.values for the accelerometer sensor.
private static final int ACCELEROMETER_DATA_X = 0;
private static final int ACCELEROMETER_DATA_Y = 1;
@@ -216,40 +226,41 @@ public abstract class WindowOrientationListener {
private final WindowOrientationListener mOrientationListener;
- /* State for first order low-pass filtering of accelerometer data.
- * See http://en.wikipedia.org/wiki/Low-pass_filter#Discrete-time_realization for
- * signal processing background.
- */
-
- private long mLastTimestamp = Long.MAX_VALUE; // in nanoseconds
- private float mLastFilteredX, mLastFilteredY, mLastFilteredZ;
-
- // The current proposal. We wait for the proposal to be stable for a
- // certain amount of time before accepting it.
- //
- // The basic idea is to ignore intermediate poses of the device while the
- // user is picking up, putting down or turning the device.
- private long mProposalTime;
- private int mProposalRotation;
- private long mProposalAgeMS;
- private boolean mProposalSettled;
-
- // A historical trace of tilt and orientation angles. Used to determine whether
- // the device posture has settled down.
- private static final int HISTORY_SIZE = 20;
- private int mHistoryIndex; // index of most recent sample
- private int mHistoryLength; // length of historical trace
- private final long[] mHistoryTimestampMS = new long[HISTORY_SIZE];
- private final float[] mHistoryMagnitudes = new float[HISTORY_SIZE];
- private final int[] mHistoryTiltAngles = new int[HISTORY_SIZE];
- private final int[] mHistoryOrientationAngles = new int[HISTORY_SIZE];
+ // The minimum amount of time that a predicted rotation must be stable before it
+ // is accepted as a valid rotation proposal. This value can be quite small because
+ // the low-pass filter already suppresses most of the noise so we're really just
+ // looking for quick confirmation that the last few samples are in agreement as to
+ // the desired orientation.
+ private static final long PROPOSAL_SETTLE_TIME_NANOS = 40 * NANOS_PER_MS;
+
+ // The minimum amount of time that must have elapsed since the device last exited
+ // the flat state (time since it was picked up) before the proposed rotation
+ // can change.
+ private static final long PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS = 500 * NANOS_PER_MS;
+
+ // The mininum amount of time that must have elapsed since the device stopped
+ // swinging (time since device appeared to be in the process of being put down
+ // or put away into a pocket) before the proposed rotation can change.
+ private static final long PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS = 300 * NANOS_PER_MS;
+
+ // If the tilt angle remains greater than the specified angle for a minimum of
+ // the specified time, then the device is deemed to be lying flat
+ // (just chillin' on a table).
+ private static final float FLAT_ANGLE = 75;
+ private static final long FLAT_TIME_NANOS = 1000 * NANOS_PER_MS;
+
+ // If the tilt angle has increased by at least delta degrees within the specified amount
+ // of time, then the device is deemed to be swinging away from the user
+ // down towards flat (tilt = 90).
+ private static final float SWING_AWAY_ANGLE_DELTA = 20;
+ private static final long SWING_TIME_NANOS = 300 * NANOS_PER_MS;
// The maximum sample inter-arrival time in milliseconds.
// If the acceleration samples are further apart than this amount in time, we reset the
// state of the low-pass filter and orientation properties. This helps to handle
// boundary conditions when the device is turned on, wakes from suspend or there is
// a significant gap in samples.
- private static final float MAX_FILTER_DELTA_TIME_MS = 1000;
+ private static final long MAX_FILTER_DELTA_TIME_NANOS = 1000 * NANOS_PER_MS;
// The acceleration filter time constant.
//
@@ -269,8 +280,10 @@ public abstract class WindowOrientationListener {
//
// Filtering adds latency proportional the time constant (inversely proportional
// to the cutoff frequency) so we don't want to make the time constant too
- // large or we can lose responsiveness.
- private static final float FILTER_TIME_CONSTANT_MS = 100.0f;
+ // large or we can lose responsiveness. Likewise we don't want to make it too
+ // small or we do a poor job suppressing acceleration spikes.
+ // Empirically, 100ms seems to be too small and 500ms is too large.
+ private static final float FILTER_TIME_CONSTANT_MS = 200.0f;
/* State for orientation detection. */
@@ -288,9 +301,9 @@ public abstract class WindowOrientationListener {
//
// In both cases, we postpone choosing an orientation.
private static final float MIN_ACCELERATION_MAGNITUDE =
- SensorManager.STANDARD_GRAVITY * 0.5f;
+ SensorManager.STANDARD_GRAVITY * 0.3f;
private static final float MAX_ACCELERATION_MAGNITUDE =
- SensorManager.STANDARD_GRAVITY * 1.5f;
+ SensorManager.STANDARD_GRAVITY * 1.25f;
// Maximum absolute tilt angle at which to consider orientation data. Beyond this (i.e.
// when screen is facing the sky or ground), we completely ignore orientation data.
@@ -321,33 +334,38 @@ public abstract class WindowOrientationListener {
// orientation.
private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45;
- // The number of milliseconds for which the device posture must be stable
- // before we perform an orientation change. If the device appears to be rotating
- // (being picked up, put down) then we keep waiting until it settles.
- private static final int SETTLE_TIME_MIN_MS = 200;
+ // Timestamp and value of the last accelerometer sample.
+ private long mLastFilteredTimestampNanos;
+ private float mLastFilteredX, mLastFilteredY, mLastFilteredZ;
+
+ // The last proposed rotation, -1 if unknown.
+ private int mProposedRotation;
+
+ // Value of the current predicted rotation, -1 if unknown.
+ private int mPredictedRotation;
- // The maximum number of milliseconds to wait for the posture to settle before
- // accepting the current proposal regardless.
- private static final int SETTLE_TIME_MAX_MS = 500;
+ // Timestamp of when the predicted rotation most recently changed.
+ private long mPredictedRotationTimestampNanos;
- // The maximum change in magnitude that can occur during the settle time.
- // Tuning this constant particularly helps to filter out situations where the
- // device is being picked up or put down by the user.
- private static final float SETTLE_MAGNITUDE_MAX_DELTA =
- SensorManager.STANDARD_GRAVITY * 0.2f;
+ // Timestamp when the device last appeared to be flat for sure (the flat delay elapsed).
+ private long mFlatTimestampNanos;
- // The maximum change in tilt angle that can occur during the settle time.
- private static final int SETTLE_TILT_ANGLE_MAX_DELTA = 8;
+ // Timestamp when the device last appeared to be swinging.
+ private long mSwingTimestampNanos;
- // The maximum change in orientation angle that can occur during the settle time.
- private static final int SETTLE_ORIENTATION_ANGLE_MAX_DELTA = 8;
+ // History of observed tilt angles.
+ private static final int TILT_HISTORY_SIZE = 40;
+ private float[] mTiltHistory = new float[TILT_HISTORY_SIZE];
+ private long[] mTiltHistoryTimestampNanos = new long[TILT_HISTORY_SIZE];
+ private int mTiltHistoryIndex;
public SensorEventListenerImpl(WindowOrientationListener orientationListener) {
mOrientationListener = orientationListener;
+ reset();
}
public int getProposedRotation() {
- return mProposalSettled ? mProposalRotation : -1;
+ return mProposedRotation;
}
@Override
@@ -365,8 +383,9 @@ public abstract class WindowOrientationListener {
float z = event.values[ACCELEROMETER_DATA_Z];
if (log) {
- Slog.v(TAG, "Raw acceleration vector: " +
- "x=" + x + ", y=" + y + ", z=" + z);
+ Slog.v(TAG, "Raw acceleration vector: "
+ + "x=" + x + ", y=" + y + ", z=" + z
+ + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z));
}
// Apply a low-pass filter to the acceleration up vector in cartesian space.
@@ -374,14 +393,16 @@ public abstract class WindowOrientationListener {
// or when we see values of (0, 0, 0) which indicates that we polled the
// accelerometer too soon after turning it on and we don't have any data yet.
final long now = event.timestamp;
- final float timeDeltaMS = (now - mLastTimestamp) * 0.000001f;
- boolean skipSample;
- if (timeDeltaMS <= 0 || timeDeltaMS > MAX_FILTER_DELTA_TIME_MS
+ final long then = mLastFilteredTimestampNanos;
+ final float timeDeltaMS = (now - then) * 0.000001f;
+ final boolean skipSample;
+ if (now < then
+ || now > then + MAX_FILTER_DELTA_TIME_NANOS
|| (x == 0 && y == 0 && z == 0)) {
if (log) {
Slog.v(TAG, "Resetting orientation listener.");
}
- clearProposal();
+ reset();
skipSample = true;
} else {
final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS);
@@ -389,27 +410,28 @@ public abstract class WindowOrientationListener {
y = alpha * (y - mLastFilteredY) + mLastFilteredY;
z = alpha * (z - mLastFilteredZ) + mLastFilteredZ;
if (log) {
- Slog.v(TAG, "Filtered acceleration vector: " +
- "x=" + x + ", y=" + y + ", z=" + z);
+ Slog.v(TAG, "Filtered acceleration vector: "
+ + "x=" + x + ", y=" + y + ", z=" + z
+ + ", magnitude=" + FloatMath.sqrt(x * x + y * y + z * z));
}
skipSample = false;
}
- mLastTimestamp = now;
+ mLastFilteredTimestampNanos = now;
mLastFilteredX = x;
mLastFilteredY = y;
mLastFilteredZ = z;
- final int oldProposedRotation = getProposedRotation();
+ boolean isFlat = false;
+ boolean isSwinging = false;
if (!skipSample) {
// Calculate the magnitude of the acceleration vector.
- final float magnitude = (float) Math.sqrt(x * x + y * y + z * z);
+ final float magnitude = FloatMath.sqrt(x * x + y * y + z * z);
if (magnitude < MIN_ACCELERATION_MAGNITUDE
|| magnitude > MAX_ACCELERATION_MAGNITUDE) {
if (log) {
- Slog.v(TAG, "Ignoring sensor data, magnitude out of range: "
- + "magnitude=" + magnitude);
+ Slog.v(TAG, "Ignoring sensor data, magnitude out of range.");
}
- clearProposal();
+ clearPredictedRotation();
} else {
// Calculate the tilt angle.
// This is the angle between the up vector and the x-y plane (the plane of
@@ -420,14 +442,25 @@ public abstract class WindowOrientationListener {
final int tiltAngle = (int) Math.round(
Math.asin(z / magnitude) * RADIANS_TO_DEGREES);
+ // Determine whether the device appears to be flat or swinging.
+ if (isFlat(now)) {
+ isFlat = true;
+ mFlatTimestampNanos = now;
+ }
+ if (isSwinging(now, tiltAngle)) {
+ isSwinging = true;
+ mSwingTimestampNanos = now;
+ }
+ addTiltHistoryEntry(now, tiltAngle);
+
// If the tilt angle is too close to horizontal then we cannot determine
// the orientation angle of the screen.
if (Math.abs(tiltAngle) > MAX_TILT) {
if (log) {
Slog.v(TAG, "Ignoring sensor data, tilt angle too high: "
- + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle);
+ + "tiltAngle=" + tiltAngle);
}
- clearProposal();
+ clearPredictedRotation();
} else {
// Calculate the orientation angle.
// This is the angle between the x-y projection of the up vector onto
@@ -445,86 +478,93 @@ public abstract class WindowOrientationListener {
nearestRotation = 0;
}
- // Determine the proposed orientation.
- if (!isTiltAngleAcceptable(nearestRotation, tiltAngle)
- || !isOrientationAngleAcceptable(nearestRotation,
+ // Determine the predicted orientation.
+ if (isTiltAngleAcceptable(nearestRotation, tiltAngle)
+ && isOrientationAngleAcceptable(nearestRotation,
orientationAngle)) {
+ updatePredictedRotation(now, nearestRotation);
if (log) {
- Slog.v(TAG, "Ignoring sensor data, no proposal: "
- + "magnitude=" + magnitude + ", tiltAngle=" + tiltAngle
- + ", orientationAngle=" + orientationAngle);
+ Slog.v(TAG, "Predicted: "
+ + "tiltAngle=" + tiltAngle
+ + ", orientationAngle=" + orientationAngle
+ + ", predictedRotation=" + mPredictedRotation
+ + ", predictedRotationAgeMS="
+ + ((now - mPredictedRotationTimestampNanos)
+ * 0.000001f));
}
- clearProposal();
} else {
if (log) {
- Slog.v(TAG, "Proposal: "
- + "magnitude=" + magnitude
- + ", tiltAngle=" + tiltAngle
- + ", orientationAngle=" + orientationAngle
- + ", proposalRotation=" + mProposalRotation);
+ Slog.v(TAG, "Ignoring sensor data, no predicted rotation: "
+ + "tiltAngle=" + tiltAngle
+ + ", orientationAngle=" + orientationAngle);
}
- updateProposal(nearestRotation, now / 1000000L,
- magnitude, tiltAngle, orientationAngle);
+ clearPredictedRotation();
}
}
}
}
+ // Determine new proposed rotation.
+ final int oldProposedRotation = mProposedRotation;
+ if (mPredictedRotation < 0 || isPredictedRotationAcceptable(now)) {
+ mProposedRotation = mPredictedRotation;
+ }
+
// Write final statistics about where we are in the orientation detection process.
- final int proposedRotation = getProposedRotation();
if (log) {
- final float proposalConfidence = Math.min(
- mProposalAgeMS * 1.0f / SETTLE_TIME_MIN_MS, 1.0f);
Slog.v(TAG, "Result: currentRotation=" + mOrientationListener.mCurrentRotation
- + ", proposedRotation=" + proposedRotation
+ + ", proposedRotation=" + mProposedRotation
+ + ", predictedRotation=" + mPredictedRotation
+ ", timeDeltaMS=" + timeDeltaMS
- + ", proposalRotation=" + mProposalRotation
- + ", proposalAgeMS=" + mProposalAgeMS
- + ", proposalConfidence=" + proposalConfidence);
+ + ", isFlat=" + isFlat
+ + ", isSwinging=" + isSwinging
+ + ", timeUntilSettledMS=" + remainingMS(now,
+ mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS)
+ + ", timeUntilFlatDelayExpiredMS=" + remainingMS(now,
+ mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS)
+ + ", timeUntilSwingDelayExpiredMS=" + remainingMS(now,
+ mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS));
}
// Tell the listener.
- if (proposedRotation != oldProposedRotation && proposedRotation >= 0) {
+ if (mProposedRotation != oldProposedRotation && mProposedRotation >= 0) {
if (log) {
- Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + proposedRotation
+ Slog.v(TAG, "Proposed rotation changed! proposedRotation=" + mProposedRotation
+ ", oldProposedRotation=" + oldProposedRotation);
}
- mOrientationListener.onProposedRotationChanged(proposedRotation);
+ mOrientationListener.onProposedRotationChanged(mProposedRotation);
}
}
/**
- * Returns true if the tilt angle is acceptable for a proposed
- * orientation transition.
+ * Returns true if the tilt angle is acceptable for a given predicted rotation.
*/
- private boolean isTiltAngleAcceptable(int proposedRotation,
- int tiltAngle) {
- return tiltAngle >= TILT_TOLERANCE[proposedRotation][0]
- && tiltAngle <= TILT_TOLERANCE[proposedRotation][1];
+ private boolean isTiltAngleAcceptable(int rotation, int tiltAngle) {
+ return tiltAngle >= TILT_TOLERANCE[rotation][0]
+ && tiltAngle <= TILT_TOLERANCE[rotation][1];
}
/**
- * Returns true if the orientation angle is acceptable for a proposed
- * orientation transition.
+ * Returns true if the orientation angle is acceptable for a given predicted rotation.
*
* This function takes into account the gap between adjacent orientations
* for hysteresis.
*/
- private boolean isOrientationAngleAcceptable(int proposedRotation, int orientationAngle) {
+ private boolean isOrientationAngleAcceptable(int rotation, int orientationAngle) {
// If there is no current rotation, then there is no gap.
// The gap is used only to introduce hysteresis among advertised orientation
// changes to avoid flapping.
final int currentRotation = mOrientationListener.mCurrentRotation;
if (currentRotation >= 0) {
- // If the proposed rotation is the same or is counter-clockwise adjacent,
- // then we set a lower bound on the orientation angle.
+ // If the specified rotation is the same or is counter-clockwise adjacent
+ // to the current rotation, then we set a lower bound on the orientation angle.
// For example, if currentRotation is ROTATION_0 and proposed is ROTATION_90,
// then we want to check orientationAngle > 45 + GAP / 2.
- if (proposedRotation == currentRotation
- || proposedRotation == (currentRotation + 1) % 4) {
- int lowerBound = proposedRotation * 90 - 45
+ if (rotation == currentRotation
+ || rotation == (currentRotation + 1) % 4) {
+ int lowerBound = rotation * 90 - 45
+ ADJACENT_ORIENTATION_ANGLE_GAP / 2;
- if (proposedRotation == 0) {
+ if (rotation == 0) {
if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) {
return false;
}
@@ -535,15 +575,15 @@ public abstract class WindowOrientationListener {
}
}
- // If the proposed rotation is the same or is clockwise adjacent,
+ // If the specified rotation is the same or is clockwise adjacent,
// then we set an upper bound on the orientation angle.
- // For example, if currentRotation is ROTATION_0 and proposed is ROTATION_270,
+ // For example, if currentRotation is ROTATION_0 and rotation is ROTATION_270,
// then we want to check orientationAngle < 315 - GAP / 2.
- if (proposedRotation == currentRotation
- || proposedRotation == (currentRotation + 3) % 4) {
- int upperBound = proposedRotation * 90 + 45
+ if (rotation == currentRotation
+ || rotation == (currentRotation + 3) % 4) {
+ int upperBound = rotation * 90 + 45
- ADJACENT_ORIENTATION_ANGLE_GAP / 2;
- if (proposedRotation == 0) {
+ if (rotation == 0) {
if (orientationAngle <= 45 && orientationAngle > upperBound) {
return false;
}
@@ -557,66 +597,97 @@ public abstract class WindowOrientationListener {
return true;
}
- private void clearProposal() {
- mProposalRotation = -1;
- mProposalAgeMS = 0;
- mProposalSettled = false;
- }
+ /**
+ * Returns true if the predicted rotation is ready to be advertised as a
+ * proposed rotation.
+ */
+ private boolean isPredictedRotationAcceptable(long now) {
+ // The predicted rotation must have settled long enough.
+ if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) {
+ return false;
+ }
- private void updateProposal(int rotation, long timestampMS,
- float magnitude, int tiltAngle, int orientationAngle) {
- if (mProposalRotation != rotation) {
- mProposalTime = timestampMS;
- mProposalRotation = rotation;
- mHistoryIndex = 0;
- mHistoryLength = 0;
+ // The last flat state (time since picked up) must have been sufficiently long ago.
+ if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) {
+ return false;
}
- final int index = mHistoryIndex;
- mHistoryTimestampMS[index] = timestampMS;
- mHistoryMagnitudes[index] = magnitude;
- mHistoryTiltAngles[index] = tiltAngle;
- mHistoryOrientationAngles[index] = orientationAngle;
- mHistoryIndex = (index + 1) % HISTORY_SIZE;
- if (mHistoryLength < HISTORY_SIZE) {
- mHistoryLength += 1;
+ // The last swing state (time since last movement to put down) must have been
+ // sufficiently long ago.
+ if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) {
+ return false;
}
- long age = 0;
- for (int i = 1; i < mHistoryLength; i++) {
- final int olderIndex = (index + HISTORY_SIZE - i) % HISTORY_SIZE;
- if (Math.abs(mHistoryMagnitudes[olderIndex] - magnitude)
- > SETTLE_MAGNITUDE_MAX_DELTA) {
+ // Looks good!
+ return true;
+ }
+
+ private void reset() {
+ mLastFilteredTimestampNanos = Long.MIN_VALUE;
+ mProposedRotation = -1;
+ mFlatTimestampNanos = Long.MIN_VALUE;
+ mSwingTimestampNanos = Long.MIN_VALUE;
+ clearPredictedRotation();
+ clearTiltHistory();
+ }
+
+ private void clearPredictedRotation() {
+ mPredictedRotation = -1;
+ mPredictedRotationTimestampNanos = Long.MIN_VALUE;
+ }
+
+ private void updatePredictedRotation(long now, int rotation) {
+ if (mPredictedRotation != rotation) {
+ mPredictedRotation = rotation;
+ mPredictedRotationTimestampNanos = now;
+ }
+ }
+
+ private void clearTiltHistory() {
+ mTiltHistoryTimestampNanos[0] = Long.MIN_VALUE;
+ mTiltHistoryIndex = 1;
+ }
+
+ private void addTiltHistoryEntry(long now, float tilt) {
+ mTiltHistory[mTiltHistoryIndex] = tilt;
+ mTiltHistoryTimestampNanos[mTiltHistoryIndex] = now;
+ mTiltHistoryIndex = (mTiltHistoryIndex + 1) % TILT_HISTORY_SIZE;
+ mTiltHistoryTimestampNanos[mTiltHistoryIndex] = Long.MIN_VALUE;
+ }
+
+ private boolean isFlat(long now) {
+ for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) {
+ if (mTiltHistory[i] < FLAT_ANGLE) {
break;
}
- if (angleAbsoluteDelta(mHistoryTiltAngles[olderIndex],
- tiltAngle) > SETTLE_TILT_ANGLE_MAX_DELTA) {
- break;
+ if (mTiltHistoryTimestampNanos[i] + FLAT_TIME_NANOS <= now) {
+ // Tilt has remained greater than FLAT_TILT_ANGLE for FLAT_TIME_NANOS.
+ return true;
}
- if (angleAbsoluteDelta(mHistoryOrientationAngles[olderIndex],
- orientationAngle) > SETTLE_ORIENTATION_ANGLE_MAX_DELTA) {
+ }
+ return false;
+ }
+
+ private boolean isSwinging(long now, float tilt) {
+ for (int i = mTiltHistoryIndex; (i = nextTiltHistoryIndex(i)) >= 0; ) {
+ if (mTiltHistoryTimestampNanos[i] + SWING_TIME_NANOS < now) {
break;
}
- age = timestampMS - mHistoryTimestampMS[olderIndex];
- if (age >= SETTLE_TIME_MIN_MS) {
- break;
+ if (mTiltHistory[i] + SWING_AWAY_ANGLE_DELTA <= tilt) {
+ // Tilted away by SWING_AWAY_ANGLE_DELTA within SWING_TIME_NANOS.
+ return true;
}
}
- mProposalAgeMS = age;
- if (age >= SETTLE_TIME_MIN_MS
- || timestampMS - mProposalTime >= SETTLE_TIME_MAX_MS) {
- mProposalSettled = true;
- } else {
- mProposalSettled = false;
- }
+ return false;
}
- private static int angleAbsoluteDelta(int a, int b) {
- int delta = Math.abs(a - b);
- if (delta > 180) {
- delta = 360 - delta;
- }
- return delta;
+ private int nextTiltHistoryIndex(int index) {
+ index = (index == 0 ? TILT_HISTORY_SIZE : index) - 1;
+ return mTiltHistoryTimestampNanos[index] != Long.MIN_VALUE ? index : -1;
+ }
+
+ private static float remainingMS(long now, long until) {
+ return now >= until ? 0 : (until - now) * 0.000001f;
}
}
}
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index 91dcac8..75b875a 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -844,7 +844,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
record.mParcelableData = parcel.readParcelable(null);
parcel.readList(record.mText, null);
record.mSourceWindowId = parcel.readInt();
- record.mSourceViewId = parcel.readInt();
+ record.mSourceNodeId = parcel.readLong();
record.mSealed = (parcel.readInt() == 1);
}
@@ -893,7 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
parcel.writeParcelable(record.mParcelableData, flags);
parcel.writeList(record.mText);
parcel.writeInt(record.mSourceWindowId);
- parcel.writeInt(record.mSourceViewId);
+ parcel.writeLong(record.mSourceNodeId);
parcel.writeInt(record.mSealed ? 1 : 0);
}
@@ -914,7 +914,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
if (DEBUG) {
builder.append("\n");
builder.append("; sourceWindowId: ").append(mSourceWindowId);
- builder.append("; sourceViewId: ").append(mSourceViewId);
+ builder.append("; mSourceNodeId: ").append(mSourceNodeId);
for (int i = 0; i < mRecords.size(); i++) {
AccessibilityRecord record = mRecords.get(i);
builder.append(" Record ");
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index 96653e5..95c070c 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -22,6 +22,7 @@ import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
+import android.util.LongSparseArray;
import android.util.SparseArray;
import java.util.Collections;
@@ -73,7 +74,8 @@ public final class AccessibilityInteractionClient
private static final Object sStaticLock = new Object();
- private static AccessibilityInteractionClient sInstance;
+ private static final LongSparseArray<AccessibilityInteractionClient> sClients =
+ new LongSparseArray<AccessibilityInteractionClient>();
private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
@@ -96,17 +98,36 @@ public final class AccessibilityInteractionClient
new SparseArray<IAccessibilityServiceConnection>();
/**
- * @return The singleton of this class.
+ * @return The client for the current thread.
*/
public static AccessibilityInteractionClient getInstance() {
+ final long threadId = Thread.currentThread().getId();
+ return getInstanceForThread(threadId);
+ }
+
+ /**
+ * <strong>Note:</strong> We keep one instance per interrogating thread since
+ * the instance contains state which can lead to undesired thread interleavings.
+ * We do not have a thread local variable since other threads should be able to
+ * look up the correct client knowing a thread id. See ViewRootImpl for details.
+ *
+ * @return The client for a given <code>threadId</code>.
+ */
+ public static AccessibilityInteractionClient getInstanceForThread(long threadId) {
synchronized (sStaticLock) {
- if (sInstance == null) {
- sInstance = new AccessibilityInteractionClient();
+ AccessibilityInteractionClient client = sClients.get(threadId);
+ if (client == null) {
+ client = new AccessibilityInteractionClient();
+ sClients.put(threadId, client);
}
- return sInstance;
+ return client;
}
}
+ private AccessibilityInteractionClient() {
+ /* reducing constructor visibility */
+ }
+
/**
* Sets the message to be processed if the interacted view hierarchy
* and the interacting client are running in the same thread.
@@ -125,17 +146,18 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id.
- * @param accessibilityViewId A unique View accessibility id.
+ * @param accessibilityNodeId A unique node accessibility id
+ * (accessibility view and virtual descendant id).
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
- int accessibilityWindowId, int accessibilityViewId) {
+ int accessibilityWindowId, long accessibilityNodeId) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId(
- accessibilityWindowId, accessibilityViewId, interactionId, this,
+ accessibilityWindowId, accessibilityNodeId, interactionId, this,
Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (windowScale > 0) {
@@ -205,14 +227,14 @@ public final class AccessibilityInteractionClient
* @param text The searched text.
* @return A list of found {@link AccessibilityNodeInfo}s.
*/
- public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow(
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(
int connectionId, String text) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final float windowScale =
- connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text,
+ connection.findAccessibilityNodeInfosByTextInActiveWindow(text,
interactionId, this, Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (windowScale > 0) {
@@ -244,18 +266,18 @@ public final class AccessibilityInteractionClient
* @param connectionId The id of a connection for interacting with the system.
* @param text The searched text.
* @param accessibilityWindowId A unique window id.
- * @param accessibilityViewId A unique View accessibility id from where to start the search.
- * Use {@link android.view.View#NO_ID} to start from the root.
+ * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id) from
+ * where to start the search. Use {@link android.view.View#NO_ID} to start from the root.
* @return A list of found {@link AccessibilityNodeInfo}s.
*/
- public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId,
- String text, int accessibilityWindowId, int accessibilityViewId) {
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
+ String text, int accessibilityWindowId, long accessibilityNodeId) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
- final float windowScale = connection.findAccessibilityNodeInfosByViewText(text,
- accessibilityWindowId, accessibilityViewId, interactionId, this,
+ final float windowScale = connection.findAccessibilityNodeInfosByText(text,
+ accessibilityWindowId, accessibilityNodeId, interactionId, this,
Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (windowScale > 0) {
@@ -283,18 +305,18 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId The id of the window.
- * @param accessibilityViewId A unique View accessibility id.
+ * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id).
* @param action The action to perform.
* @return Whether the action was performed.
*/
public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
- int accessibilityViewId, int action) {
+ long accessibilityNodeId, int action) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final boolean success = connection.performAccessibilityAction(
- accessibilityWindowId, accessibilityViewId, action, interactionId, this,
+ accessibilityWindowId, accessibilityNodeId, action, interactionId, this,
Thread.currentThread().getId());
if (success) {
return getPerformAccessibilityActionResult(interactionId);
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 9b0f44a..6939c2c 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -20,7 +20,7 @@ import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
-import android.util.SparseIntArray;
+import android.util.SparseLongArray;
import android.view.View;
import java.util.Collections;
@@ -98,6 +98,59 @@ public class AccessibilityNodeInfo implements Parcelable {
private static final int PROPERTY_SCROLLABLE = 0x00000200;
+ /**
+ * Bits that provide the id of a virtual descendant of a view.
+ */
+ private static final long VIRTUAL_DESCENDANT_ID_MASK = 0xffffffff00000000L;
+
+ /**
+ * Bit shift of {@link #VIRTUAL_DESCENDANT_ID_MASK} to get to the id for a
+ * virtual descendant of a view. Such a descendant does not exist in the view
+ * hierarchy and is only reported via the accessibility APIs.
+ */
+ private static final int VIRTUAL_DESCENDANT_ID_SHIFT = 32;
+
+ /**
+ * Gets the accessibility view id which identifies a View in the view three.
+ *
+ * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}.
+ * @return The accessibility view id part of the node id.
+ *
+ * @hide
+ */
+ public static int getAccessibilityViewId(long accessibilityNodeId) {
+ return (int) accessibilityNodeId;
+ }
+
+ /**
+ * Gets the virtual descendant id which identifies an imaginary view in a
+ * containing View.
+ *
+ * @param accessibilityNodeId The id of an {@link AccessibilityNodeInfo}.
+ * @return The virtual view id part of the node id.
+ *
+ * @hide
+ */
+ public static int getVirtualDescendantId(long accessibilityNodeId) {
+ return (int) ((accessibilityNodeId & VIRTUAL_DESCENDANT_ID_MASK)
+ >> VIRTUAL_DESCENDANT_ID_SHIFT);
+ }
+
+ /**
+ * Makes a node id by shifting the <code>virtualDescendantId</code>
+ * by {@link #VIRTUAL_DESCENDANT_ID_SHIFT} and taking
+ * the bitwise or with the <code>accessibilityViewId</code>.
+ *
+ * @param accessibilityViewId A View accessibility id.
+ * @param virtualDescendantId A virtual descendant id.
+ * @return The node id.
+ *
+ * @hide
+ */
+ public static long makeNodeId(int accessibilityViewId, int virtualDescendantId) {
+ return (((long) virtualDescendantId) << VIRTUAL_DESCENDANT_ID_SHIFT) | accessibilityViewId;
+ }
+
// Housekeeping.
private static final int MAX_POOL_SIZE = 50;
private static final Object sPoolLock = new Object();
@@ -108,9 +161,10 @@ public class AccessibilityNodeInfo implements Parcelable {
private boolean mSealed;
// Data.
- private int mAccessibilityViewId = UNDEFINED;
- private int mAccessibilityWindowId = UNDEFINED;
- private int mParentAccessibilityViewId = UNDEFINED;
+ private int mWindowId = UNDEFINED;
+ private long mSourceNodeId = makeNodeId(UNDEFINED, UNDEFINED);
+ private long mParentNodeId = makeNodeId(UNDEFINED, UNDEFINED);
+
private int mBooleanProperties;
private final Rect mBoundsInParent = new Rect();
private final Rect mBoundsInScreen = new Rect();
@@ -120,7 +174,7 @@ public class AccessibilityNodeInfo implements Parcelable {
private CharSequence mText;
private CharSequence mContentDescription;
- private SparseIntArray mChildAccessibilityIds = new SparseIntArray();
+ private SparseLongArray mChildIds = new SparseLongArray();
private int mActions;
private int mConnectionId = UNDEFINED;
@@ -134,13 +188,43 @@ public class AccessibilityNodeInfo implements Parcelable {
/**
* Sets the source.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
*
* @param source The info source.
*/
public void setSource(View source) {
+ setSource(source, UNDEFINED);
+ }
+
+ /**
+ * Sets the source to be a virtual descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root
+ * is set as the source.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report themselves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setSource(View root, int virtualDescendantId) {
enforceNotSealed();
- mAccessibilityViewId = source.getAccessibilityViewId();
- mAccessibilityWindowId = source.getAccessibilityWindowId();
+ mWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED;
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED;
+ mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
}
/**
@@ -149,7 +233,7 @@ public class AccessibilityNodeInfo implements Parcelable {
* @return The window id.
*/
public int getWindowId() {
- return mAccessibilityWindowId;
+ return mWindowId;
}
/**
@@ -158,7 +242,7 @@ public class AccessibilityNodeInfo implements Parcelable {
* @return The child count.
*/
public int getChildCount() {
- return mChildAccessibilityIds.size();
+ return mChildIds.size();
}
/**
@@ -177,21 +261,20 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
public AccessibilityNodeInfo getChild(int index) {
enforceSealed();
- final int childAccessibilityViewId = mChildAccessibilityIds.get(index);
- if (!canPerformRequestOverConnection(childAccessibilityViewId)) {
+ if (!canPerformRequestOverConnection(mSourceNodeId)) {
return null;
}
+ final long childId = mChildIds.get(index);
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
- return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
- mAccessibilityWindowId, childAccessibilityViewId);
+ return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mWindowId, childId);
}
/**
* Adds a child.
* <p>
- * <strong>Note:</strong> Cannot be called from an
- * {@link android.accessibilityservice.AccessibilityService}.
- * This class is made immutable before being delivered to an AccessibilityService.
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
* </p>
*
* @param child The child.
@@ -199,10 +282,30 @@ public class AccessibilityNodeInfo implements Parcelable {
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void addChild(View child) {
+ addChild(child, UNDEFINED);
+ }
+
+ /**
+ * Adds a virtual child which is a descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> is {@link View#NO_ID} the root
+ * is added as a child.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual child.
+ */
+ public void addChild(View root, int virtualDescendantId) {
enforceNotSealed();
- final int childAccessibilityViewId = child.getAccessibilityViewId();
- final int index = mChildAccessibilityIds.size();
- mChildAccessibilityIds.put(index, childAccessibilityViewId);
+ final int index = mChildIds.size();
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED;
+ final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+ mChildIds.put(index, childNodeId);
}
/**
@@ -250,12 +353,11 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
public boolean performAction(int action) {
enforceSealed();
- if (!canPerformRequestOverConnection(mAccessibilityViewId)) {
+ if (!canPerformRequestOverConnection(mSourceNodeId)) {
return false;
}
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
- return client.performAccessibilityAction(mConnectionId, mAccessibilityWindowId,
- mAccessibilityViewId, action);
+ return client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId, action);
}
/**
@@ -274,12 +376,12 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) {
enforceSealed();
- if (!canPerformRequestOverConnection(mAccessibilityViewId)) {
+ if (!canPerformRequestOverConnection(mSourceNodeId)) {
return Collections.emptyList();
}
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
- return client.findAccessibilityNodeInfosByViewText(mConnectionId, text,
- mAccessibilityWindowId, mAccessibilityViewId);
+ return client.findAccessibilityNodeInfosByText(mConnectionId, text, mWindowId,
+ mSourceNodeId);
}
/**
@@ -294,12 +396,12 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
public AccessibilityNodeInfo getParent() {
enforceSealed();
- if (!canPerformRequestOverConnection(mParentAccessibilityViewId)) {
+ if (!canPerformRequestOverConnection(mParentNodeId)) {
return null;
}
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
- mAccessibilityWindowId, mParentAccessibilityViewId);
+ mWindowId, mParentNodeId);
}
/**
@@ -315,8 +417,33 @@ public class AccessibilityNodeInfo implements Parcelable {
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setParent(View parent) {
+ setParent(parent, UNDEFINED);
+ }
+
+ /**
+ * Sets the parent to be a virtual descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root
+ * is set as the parent.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setParent(View root, int virtualDescendantId) {
enforceNotSealed();
- mParentAccessibilityViewId = parent.getAccessibilityViewId();
+ final int rootAccessibilityViewId =
+ (root != null) ? root.getAccessibilityViewId() : UNDEFINED;
+ mParentNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
}
/**
@@ -829,6 +956,7 @@ public class AccessibilityNodeInfo implements Parcelable {
* Returns a cached instance if such is available otherwise a new one
* and sets the source.
*
+ * @param source The source view.
* @return An instance.
*
* @see #setSource(View)
@@ -840,6 +968,22 @@ public class AccessibilityNodeInfo implements Parcelable {
}
/**
+ * Returns a cached instance if such is available otherwise a new one
+ * and sets the source.
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ * @return An instance.
+ *
+ * @see #setSource(View, int)
+ */
+ public static AccessibilityNodeInfo obtain(View root, int virtualDescendantId) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setSource(root, virtualDescendantId);
+ return info;
+ }
+
+ /**
* Returns a cached instance if such is available otherwise a new one.
*
* @return An instance.
@@ -903,16 +1047,16 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(isSealed() ? 1 : 0);
- parcel.writeInt(mAccessibilityViewId);
- parcel.writeInt(mAccessibilityWindowId);
- parcel.writeInt(mParentAccessibilityViewId);
+ parcel.writeLong(mSourceNodeId);
+ parcel.writeInt(mWindowId);
+ parcel.writeLong(mParentNodeId);
parcel.writeInt(mConnectionId);
- SparseIntArray childIds = mChildAccessibilityIds;
+ SparseLongArray childIds = mChildIds;
final int childIdsSize = childIds.size();
parcel.writeInt(childIdsSize);
for (int i = 0; i < childIdsSize; i++) {
- parcel.writeInt(childIds.valueAt(i));
+ parcel.writeLong(childIds.valueAt(i));
}
parcel.writeInt(mBoundsInParent.top);
@@ -946,9 +1090,9 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
private void init(AccessibilityNodeInfo other) {
mSealed = other.mSealed;
- mAccessibilityViewId = other.mAccessibilityViewId;
- mParentAccessibilityViewId = other.mParentAccessibilityViewId;
- mAccessibilityWindowId = other.mAccessibilityWindowId;
+ mSourceNodeId = other.mSourceNodeId;
+ mParentNodeId = other.mParentNodeId;
+ mWindowId = other.mWindowId;
mConnectionId = other.mConnectionId;
mBoundsInParent.set(other.mBoundsInParent);
mBoundsInScreen.set(other.mBoundsInScreen);
@@ -958,7 +1102,7 @@ public class AccessibilityNodeInfo implements Parcelable {
mContentDescription = other.mContentDescription;
mActions= other.mActions;
mBooleanProperties = other.mBooleanProperties;
- mChildAccessibilityIds = other.mChildAccessibilityIds.clone();
+ mChildIds = other.mChildIds.clone();
}
/**
@@ -968,15 +1112,15 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
private void initFromParcel(Parcel parcel) {
mSealed = (parcel.readInt() == 1);
- mAccessibilityViewId = parcel.readInt();
- mAccessibilityWindowId = parcel.readInt();
- mParentAccessibilityViewId = parcel.readInt();
+ mSourceNodeId = parcel.readLong();
+ mWindowId = parcel.readInt();
+ mParentNodeId = parcel.readLong();
mConnectionId = parcel.readInt();
- SparseIntArray childIds = mChildAccessibilityIds;
+ SparseLongArray childIds = mChildIds;
final int childrenSize = parcel.readInt();
for (int i = 0; i < childrenSize; i++) {
- final int childId = parcel.readInt();
+ final long childId = parcel.readLong();
childIds.put(i, childId);
}
@@ -1005,11 +1149,11 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
private void clear() {
mSealed = false;
- mAccessibilityViewId = UNDEFINED;
- mParentAccessibilityViewId = UNDEFINED;
- mAccessibilityWindowId = UNDEFINED;
+ mSourceNodeId = makeNodeId(UNDEFINED, UNDEFINED);
+ mParentNodeId = makeNodeId(UNDEFINED, UNDEFINED);
+ mWindowId = UNDEFINED;
mConnectionId = UNDEFINED;
- mChildAccessibilityIds.clear();
+ mChildIds.clear();
mBoundsInParent.set(0, 0, 0, 0);
mBoundsInScreen.set(0, 0, 0, 0);
mBooleanProperties = 0;
@@ -1041,9 +1185,10 @@ public class AccessibilityNodeInfo implements Parcelable {
}
}
- private boolean canPerformRequestOverConnection(int accessibilityViewId) {
- return (mConnectionId != UNDEFINED && mAccessibilityWindowId != UNDEFINED
- && accessibilityViewId != UNDEFINED);
+ private boolean canPerformRequestOverConnection(long accessibilityNodeId) {
+ return (mWindowId != UNDEFINED
+ && getAccessibilityViewId(accessibilityNodeId) != UNDEFINED
+ && mConnectionId != UNDEFINED);
}
@Override
@@ -1058,10 +1203,10 @@ public class AccessibilityNodeInfo implements Parcelable {
return false;
}
AccessibilityNodeInfo other = (AccessibilityNodeInfo) object;
- if (mAccessibilityViewId != other.mAccessibilityViewId) {
+ if (mSourceNodeId != other.mSourceNodeId) {
return false;
}
- if (mAccessibilityWindowId != other.mAccessibilityWindowId) {
+ if (mWindowId != other.mWindowId) {
return false;
}
return true;
@@ -1071,8 +1216,9 @@ public class AccessibilityNodeInfo implements Parcelable {
public int hashCode() {
final int prime = 31;
int result = 1;
- result = prime * result + mAccessibilityViewId;
- result = prime * result + mAccessibilityWindowId;
+ result = prime * result + getAccessibilityViewId(mSourceNodeId);
+ result = prime * result + getVirtualDescendantId(mSourceNodeId);
+ result = prime * result + mWindowId;
return result;
}
@@ -1082,9 +1228,10 @@ public class AccessibilityNodeInfo implements Parcelable {
builder.append(super.toString());
if (DEBUG) {
- builder.append("; accessibilityId: " + mAccessibilityViewId);
- builder.append("; parentAccessibilityId: " + mParentAccessibilityViewId);
- SparseIntArray childIds = mChildAccessibilityIds;
+ builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId));
+ builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId));
+ builder.append("; mParentNodeId: " + mParentNodeId);
+ SparseLongArray childIds = mChildIds;
builder.append("; childAccessibilityIds: [");
for (int i = 0, count = childIds.size(); i < count; i++) {
builder.append(childIds.valueAt(i));
diff --git a/core/java/android/view/accessibility/AccessibilityNodeProvider.java b/core/java/android/view/accessibility/AccessibilityNodeProvider.java
new file mode 100644
index 0000000..5890417
--- /dev/null
+++ b/core/java/android/view/accessibility/AccessibilityNodeProvider.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.accessibilityservice.AccessibilityService;
+import android.view.View;
+
+import java.util.List;
+
+/**
+ * This class is the contract a client should implement to enable support of a
+ * virtual view hierarchy rooted at a given view for accessibility purposes. A virtual
+ * view hierarchy is a tree of imaginary Views that is reported as a part of the view
+ * hierarchy when an {@link AccessibilityService} explores the window content.
+ * Since the virtual View tree does not exist this class is responsible for
+ * managing the {@link AccessibilityNodeInfo}s describing that tree to accessibility
+ * services.
+ * </p>
+ * <p>
+ * The main use case of these APIs is to enable a custom view that draws complex content,
+ * for example a monthly calendar grid, to be presented as a tree of logical nodes,
+ * for example month days each containing events, thus conveying its logical structure.
+ * <p>
+ * <p>
+ * A typical use case is to override {@link View#getAccessibilityNodeProvider()} of the
+ * View that is a root of a virtual View hierarchy to return an instance of this class.
+ * In such a case this instance is responsible for managing {@link AccessibilityNodeInfo}s
+ * describing the virtual sub-tree rooted at the View including the one representing the
+ * View itself. Similarly the returned instance is responsible for performing accessibility
+ * actions on any virtual view or the root view itself. For example:
+ * </p>
+ * <pre>
+ * getAccessibilityNodeProvider(
+ * if (mAccessibilityNodeProvider == null) {
+ * mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+ * public boolean performAccessibilityAction(int action, int virtualDescendantId) {
+ * // Implementation.
+ * return false;
+ * }
+ *
+ * public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text, int virtualDescendantId) {
+ * // Implementation.
+ * return null;
+ * }
+ *
+ * public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
+ * // Implementation.
+ * return null;
+ * }
+ * });
+ * return mAccessibilityNodeProvider;
+ * </pre>
+ */
+public abstract class AccessibilityNodeProvider {
+
+ /**
+ * Returns an {@link AccessibilityNodeInfo} representing a virtual view,
+ * i.e. a descendant of the host View, with the given <code>virtualViewId</code>
+ * or the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ * <p>
+ * The implementer is responsible for obtaining an accessibility node info from the
+ * pool of reusable instances and setting the desired properties of the node info
+ * before returning it.
+ * </p>
+ *
+ * @param virtualViewId A client defined virtual view id.
+ * @return A populated {@link AccessibilityNodeInfo} for a virtual descendant or the
+ * host View.
+ *
+ * @see AccessibilityNodeInfo
+ */
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ return null;
+ }
+
+ /**
+ * Performs an accessibility action on a virtual view, i.e. a descendant of the
+ * host View, with the given <code>virtualViewId</code> or the host View itself
+ * if <code>virtualViewId</code> equals to {@link View#NO_ID}.
+ *
+ * @param action The action to perform.
+ * @param virtualViewId A client defined virtual view id.
+ * @return True if the action was performed.
+ *
+ * @see #createAccessibilityNodeInfo(int)
+ * @see AccessibilityNodeInfo
+ */
+ public boolean performAccessibilityAction(int action, int virtualViewId) {
+ return false;
+ }
+
+ /**
+ * Finds {@link AccessibilityNodeInfo}s by text. The match is case insensitive
+ * containment. The search is relative to the virtual view, i.e. a descendant of the
+ * host View, with the given <code>virtualViewId</code> or the host View itself
+ * <code>virtualViewId</code> equals to {@link View#NO_ID}.
+ *
+ * @param virtualViewId A client defined virtual view id which defined
+ * the root of the tree in which to perform the search.
+ * @param text The searched text.
+ * @return A list of node info.
+ *
+ * @see #createAccessibilityNodeInfo(int)
+ * @see AccessibilityNodeInfo
+ */
+ public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text,
+ int virtualViewId) {
+ return null;
+ }
+}
diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java
index 18d0f6f..07aeb9a 100644
--- a/core/java/android/view/accessibility/AccessibilityRecord.java
+++ b/core/java/android/view/accessibility/AccessibilityRecord.java
@@ -77,7 +77,7 @@ public class AccessibilityRecord {
int mAddedCount= UNDEFINED;
int mRemovedCount = UNDEFINED;
- int mSourceViewId = UNDEFINED;
+ long mSourceNodeId = AccessibilityNodeInfo.makeNodeId(UNDEFINED, UNDEFINED);
int mSourceWindowId = UNDEFINED;
CharSequence mClassName;
@@ -103,14 +103,28 @@ public class AccessibilityRecord {
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setSource(View source) {
+ setSource(source, UNDEFINED);
+ }
+
+ /**
+ * Sets the source to be a virtual descendant of the given <code>root</code>.
+ * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root
+ * is set as the source.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report them selves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ *
+ * @param root The root of the virtual subtree.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setSource(View root, int virtualDescendantId) {
enforceNotSealed();
- if (source != null) {
- mSourceWindowId = source.getAccessibilityWindowId();
- mSourceViewId = source.getAccessibilityViewId();
- } else {
- mSourceWindowId = UNDEFINED;
- mSourceViewId = UNDEFINED;
- }
+ mSourceWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED;
+ final int rootViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED;
+ mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId);
}
/**
@@ -125,12 +139,12 @@ public class AccessibilityRecord {
public AccessibilityNodeInfo getSource() {
enforceSealed();
if (mConnectionId == UNDEFINED || mSourceWindowId == UNDEFINED
- || mSourceViewId == UNDEFINED) {
+ || AccessibilityNodeInfo.getAccessibilityViewId(mSourceNodeId) == UNDEFINED) {
return null;
}
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mSourceWindowId,
- mSourceViewId);
+ mSourceNodeId);
}
/**
@@ -383,6 +397,7 @@ public class AccessibilityRecord {
public int getMaxScrollX() {
return mMaxScrollX;
}
+
/**
* Sets the max scroll offset of the source left edge in pixels.
*
@@ -708,7 +723,7 @@ public class AccessibilityRecord {
mParcelableData = record.mParcelableData;
mText.addAll(record.mText);
mSourceWindowId = record.mSourceWindowId;
- mSourceViewId = record.mSourceViewId;
+ mSourceNodeId = record.mSourceNodeId;
mConnectionId = record.mConnectionId;
}
@@ -733,7 +748,7 @@ public class AccessibilityRecord {
mBeforeText = null;
mParcelableData = null;
mText.clear();
- mSourceViewId = UNDEFINED;
+ mSourceNodeId = AccessibilityNodeInfo.makeNodeId(UNDEFINED, UNDEFINED);
mSourceWindowId = UNDEFINED;
mConnectionId = UNDEFINED;
}
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
index 535d594..a90c427 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
@@ -27,7 +27,7 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
*/
oneway interface IAccessibilityInteractionConnection {
- void findAccessibilityNodeInfoByAccessibilityId(int accessibilityViewId, int interactionId,
+ void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid);
@@ -35,11 +35,11 @@ oneway interface IAccessibilityInteractionConnection {
IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid);
- void findAccessibilityNodeInfosByViewText(String text, int accessibilityViewId,
+ void findAccessibilityNodeInfosByText(String text, long accessibilityNodeId,
int interactionId, IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid);
- void performAccessibilityAction(int accessibilityId, int action, int interactionId,
+ void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int interrogatingPid,
long interrogatingTid);
}
diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java
index 0d57c9b..e8c0239 100644
--- a/core/java/android/view/animation/Animation.java
+++ b/core/java/android/view/animation/Animation.java
@@ -323,7 +323,7 @@ public abstract class Animation implements Cloneable {
/**
* Initialize this animation with the dimensions of the object being
* animated as well as the objects parents. (This is to support animation
- * sizes being specifed relative to these dimensions.)
+ * sizes being specified relative to these dimensions.)
*
* <p>Objects that interpret Animations should call this method when
* the sizes of the object being animated and its parent are known, and
diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java
index 2cf8ea8..14d3d19 100644
--- a/core/java/android/view/animation/AnimationSet.java
+++ b/core/java/android/view/animation/AnimationSet.java
@@ -224,7 +224,9 @@ public class AnimationSet extends Animation {
}
boolean changeBounds = (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == 0;
- if (changeBounds && a.willChangeTransformationMatrix()) {
+
+
+ if (changeBounds && a.willChangeBounds()) {
mFlags |= PROPERTY_CHANGE_BOUNDS_MASK;
}
@@ -346,12 +348,13 @@ public class AnimationSet extends Animation {
for (int i = count - 1; i >= 0; --i) {
final Animation a = animations.get(i);
-
- temp.clear();
- final Interpolator interpolator = a.mInterpolator;
- a.applyTransformation(interpolator != null ? interpolator.getInterpolation(0.0f)
- : 0.0f, temp);
- previousTransformation.compose(temp);
+ if (!a.isFillEnabled() || a.getFillBefore() || a.getStartOffset() == 0) {
+ temp.clear();
+ final Interpolator interpolator = a.mInterpolator;
+ a.applyTransformation(interpolator != null ? interpolator.getInterpolation(0.0f)
+ : 0.0f, temp);
+ previousTransformation.compose(temp);
+ }
}
}
}
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
index 32ff647..38043b2 100644
--- a/core/java/android/view/animation/AnimationUtils.java
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -133,6 +133,14 @@ public class AnimationUtils {
}
+ /**
+ * Loads a {@link LayoutAnimationController} object from a resource
+ *
+ * @param context Application context used to access resources
+ * @param id The resource id of the animation to load
+ * @return The animation object reference by the specified id
+ * @throws NotFoundException when the layout animation controller cannot be loaded
+ */
public static LayoutAnimationController loadLayoutAnimation(Context context, int id)
throws NotFoundException {
diff --git a/core/java/android/view/animation/RotateAnimation.java b/core/java/android/view/animation/RotateAnimation.java
index 58bf084..67e0374 100644
--- a/core/java/android/view/animation/RotateAnimation.java
+++ b/core/java/android/view/animation/RotateAnimation.java
@@ -66,6 +66,8 @@ public class RotateAnimation extends Animation {
mPivotYValue = d.value;
a.recycle();
+
+ initializePivotPoint();
}
/**
@@ -107,6 +109,7 @@ public class RotateAnimation extends Animation {
mPivotYType = ABSOLUTE;
mPivotXValue = pivotX;
mPivotYValue = pivotY;
+ initializePivotPoint();
}
/**
@@ -143,6 +146,20 @@ public class RotateAnimation extends Animation {
mPivotXType = pivotXType;
mPivotYValue = pivotYValue;
mPivotYType = pivotYType;
+ initializePivotPoint();
+ }
+
+ /**
+ * Called at the end of constructor methods to initialize, if possible, values for
+ * the pivot point. This is only possible for ABSOLUTE pivot values.
+ */
+ private void initializePivotPoint() {
+ if (mPivotXType == ABSOLUTE) {
+ mPivotX = mPivotXValue;
+ }
+ if (mPivotYType == ABSOLUTE) {
+ mPivotY = mPivotYValue;
+ }
}
@Override
diff --git a/core/java/android/view/animation/ScaleAnimation.java b/core/java/android/view/animation/ScaleAnimation.java
index 1dd250f..e9a8436 100644
--- a/core/java/android/view/animation/ScaleAnimation.java
+++ b/core/java/android/view/animation/ScaleAnimation.java
@@ -128,6 +128,8 @@ public class ScaleAnimation extends Animation {
mPivotYValue = d.value;
a.recycle();
+
+ initializePivotPoint();
}
/**
@@ -178,6 +180,7 @@ public class ScaleAnimation extends Animation {
mPivotYType = ABSOLUTE;
mPivotXValue = pivotX;
mPivotYValue = pivotY;
+ initializePivotPoint();
}
/**
@@ -218,6 +221,20 @@ public class ScaleAnimation extends Animation {
mPivotXType = pivotXType;
mPivotYValue = pivotYValue;
mPivotYType = pivotYType;
+ initializePivotPoint();
+ }
+
+ /**
+ * Called at the end of constructor methods to initialize, if possible, values for
+ * the pivot point. This is only possible for ABSOLUTE pivot values.
+ */
+ private void initializePivotPoint() {
+ if (mPivotXType == ABSOLUTE) {
+ mPivotX = mPivotXValue;
+ }
+ if (mPivotYType == ABSOLUTE) {
+ mPivotY = mPivotYValue;
+ }
}
@Override
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index 5ec1ec3..bd02d62 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -193,10 +193,12 @@ public class BaseInputConnection implements InputConnection {
/**
* The default implementation performs the deletion around the current
* selection position of the editable text.
+ * @param beforeLength
+ * @param afterLength
*/
- public boolean deleteSurroundingText(int leftLength, int rightLength) {
- if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength
- + " / " + rightLength);
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
+ + " / " + afterLength);
final Editable content = getEditable();
if (content == null) return false;
@@ -226,17 +228,17 @@ public class BaseInputConnection implements InputConnection {
int deleted = 0;
- if (leftLength > 0) {
- int start = a - leftLength;
+ if (beforeLength > 0) {
+ int start = a - beforeLength;
if (start < 0) start = 0;
content.delete(start, a);
deleted = a - start;
}
- if (rightLength > 0) {
+ if (afterLength > 0) {
b = b - deleted;
- int end = b + rightLength;
+ int end = b + afterLength;
if (end > content.length()) end = content.length();
content.delete(b, end);
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index ac378fc..5146567 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -168,6 +168,22 @@ public class EditorInfo implements InputType, Parcelable {
public static final int IME_FLAG_NO_ENTER_ACTION = 0x40000000;
/**
+ * Flag of {@link #imeOptions}: used to request that the IME is capable of
+ * inputting ASCII characters. The intention of this flag is to ensure that
+ * the user can type Roman alphabet characters in a {@link android.widget.TextView}
+ * used for, typically, account ID or password input. It is expected that IMEs
+ * normally are able to input ASCII even without being told so (such IMEs
+ * already respect this flag in a sense), but there could be some cases they
+ * aren't when, for instance, only non-ASCII input languagaes like Arabic,
+ * Greek, Hebrew, Russian are enabled in the IME. Applications need to be
+ * aware that the flag is not a guarantee, and not all IMEs will respect it.
+ * However, it is strongly recommended for IME authors to respect this flag
+ * especially when their IME could end up with a state that has only non-ASCII
+ * input languages enabled.
+ */
+ public static final int IME_FLAG_FORCE_ASCII = 0x80000000;
+
+ /**
* Generic unspecified type for {@link #imeOptions}.
*/
public static final int IME_NULL = 0x00000000;
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index a6639d1..3563d4d 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -138,19 +138,21 @@ public interface InputConnection {
int flags);
/**
- * Delete <var>leftLength</var> characters of text before the current cursor
- * position, and delete <var>rightLength</var> characters of text after the
- * current cursor position, excluding composing text.
+ * Delete <var>beforeLength</var> characters of text before the current cursor
+ * position, and delete <var>afterLength</var> characters of text after the
+ * current cursor position, excluding composing text. Before and after refer
+ * to the order of the characters in the string, not to their visual representation.
*
- * @param leftLength The number of characters to be deleted before the
+ *
+ * @param beforeLength The number of characters to be deleted before the
* current cursor position.
- * @param rightLength The number of characters to be deleted after the
+ * @param afterLength The number of characters to be deleted after the
* current cursor position.
- *
+ *
* @return Returns true on success, false if the input connection is no longer
* valid.
*/
- public boolean deleteSurroundingText(int leftLength, int rightLength);
+ public boolean deleteSurroundingText(int beforeLength, int afterLength);
/**
* Set composing text around the current cursor position with the given text,
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 690ea85..a48473e 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -62,8 +62,8 @@ public class InputConnectionWrapper implements InputConnection {
return mTarget.getExtractedText(request, flags);
}
- public boolean deleteSurroundingText(int leftLength, int rightLength) {
- return mTarget.deleteSurroundingText(leftLength, rightLength);
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ return mTarget.deleteSurroundingText(beforeLength, afterLength);
}
public boolean setComposingText(CharSequence text, int newCursorPosition) {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index b41e6f5..0985e14 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -651,19 +651,7 @@ public final class InputMethodManager {
}
}
- if (mServedInputConnection != null) {
- // We need to tell the previously served view that it is no
- // longer the input target, so it can reset its state. Schedule
- // this call on its window's Handler so it will be on the correct
- // thread and outside of our lock.
- Handler vh = mServedView.getHandler();
- if (vh != null) {
- // This will result in a call to reportFinishInputConnection()
- // below.
- vh.sendMessage(vh.obtainMessage(ViewRootImpl.FINISH_INPUT_CONNECTION,
- mServedInputConnection));
- }
- }
+ notifyInputConnectionFinished();
mServedView = null;
mCompletions = null;
@@ -671,7 +659,25 @@ public final class InputMethodManager {
clearConnectionLocked();
}
}
-
+
+ /**
+ * Notifies the served view that the current InputConnection will no longer be used.
+ */
+ private void notifyInputConnectionFinished() {
+ if (mServedView != null && mServedInputConnection != null) {
+ // We need to tell the previously served view that it is no
+ // longer the input target, so it can reset its state. Schedule
+ // this call on its window's Handler so it will be on the correct
+ // thread and outside of our lock.
+ Handler vh = mServedView.getHandler();
+ if (vh != null) {
+ // This will result in a call to reportFinishInputConnection() below.
+ vh.sendMessage(vh.obtainMessage(ViewRootImpl.FINISH_INPUT_CONNECTION,
+ mServedInputConnection));
+ }
+ }
+ }
+
/**
* Called from the FINISH_INPUT_CONNECTION message above.
* @hide
@@ -681,7 +687,7 @@ public final class InputMethodManager {
ic.finishComposingText();
}
}
-
+
public void displayCompletions(View view, CompletionInfo[] completions) {
checkFocus();
synchronized (mH) {
@@ -831,7 +837,7 @@ public final class InputMethodManager {
* shown with {@link #SHOW_FORCED}.
*/
public static final int HIDE_NOT_ALWAYS = 0x0002;
-
+
/**
* Synonym for {@link #hideSoftInputFromWindow(IBinder, int, ResultReceiver)}
* without a result: request to hide the soft input window from the
@@ -993,7 +999,7 @@ public final class InputMethodManager {
tba.fieldId = view.getId();
InputConnection ic = view.onCreateInputConnection(tba);
if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
-
+
synchronized (mH) {
// Now that we are locked again, validate that our state hasn't
// changed.
@@ -1012,6 +1018,8 @@ public final class InputMethodManager {
// Hook 'em up and let 'er rip.
mCurrentTextBoxAttribute = tba;
mServedConnecting = false;
+ // Notify the served view that its previous input connection is finished
+ notifyInputConnectionFinished();
mServedInputConnection = ic;
IInputContext servedContext;
if (ic != null) {
@@ -1115,7 +1123,7 @@ public final class InputMethodManager {
}
}
- void scheduleCheckFocusLocked(View view) {
+ static void scheduleCheckFocusLocked(View view) {
Handler vh = view.getHandler();
if (vh != null && !vh.hasMessages(ViewRootImpl.CHECK_FOCUS)) {
// This will result in a call to checkFocus() below.
diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java
index 93caabe..18dec52 100644
--- a/core/java/android/view/inputmethod/InputMethodSubtype.java
+++ b/core/java/android/view/inputmethod/InputMethodSubtype.java
@@ -31,17 +31,15 @@ import java.util.List;
import java.util.Locale;
/**
- * This class is used to specify meta information of a subtype contained in an input method.
- * Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard...), and is
- * used for IME switch and settings. The input method subtype allows the system to bring up the
- * specified subtype of the designated input method directly.
+ * This class is used to specify meta information of a subtype contained in an input method editor
+ * (IME). Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard...),
+ * and is used for IME switch and settings. The input method subtype allows the system to bring up
+ * the specified subtype of the designated IME directly.
*
- * <p>It should be defined in an XML resource file of the input method
- * with the <code>&lt;subtype></code> element.
- * For more information, see the guide to
+ * <p>It should be defined in an XML resource file of the input method with the
+ * <code>&lt;subtype&gt;</code> element. For more information, see the guide to
* <a href="{@docRoot}resources/articles/creating-input-method.html">
* Creating an Input Method</a>.</p>
- *
*/
public final class InputMethodSubtype implements Parcelable {
private static final String TAG = InputMethodSubtype.class.getSimpleName();
@@ -59,13 +57,24 @@ public final class InputMethodSubtype implements Parcelable {
private HashMap<String, String> mExtraValueHashMapCache;
/**
- * Constructor
- * @param nameId The name of the subtype
- * @param iconId The icon of the subtype
+ * Constructor.
+ * @param nameId Resource ID of the subtype name string. The string resource may have exactly
+ * one %s in it. If there is, the %s part will be replaced with the locale's display name by
+ * the formatter. Please refer to {@link #getDisplayName} for details.
+ * @param iconId Resource ID of the subtype icon drawable.
* @param locale The locale supported by the subtype
* @param mode The mode supported by the subtype
- * @param extraValue The extra value of the subtype
- * @param isAuxiliary true when this subtype is one shot subtype.
+ * @param extraValue The extra value of the subtype. This string is free-form, but the API
+ * supplies tools to deal with a key-value comma-separated list; see
+ * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
+ * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary
+ * subtype will not be shown in the list of enabled IMEs for choosing the current IME in
+ * the Settings even when this subtype is enabled. Please note that this subtype will still
+ * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch
+ * to this subtype while an IME is shown. The framework will never switch the current IME to
+ * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+ * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
+ * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
* @hide
*/
public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
@@ -74,16 +83,28 @@ public final class InputMethodSubtype implements Parcelable {
}
/**
- * Constructor
- * @param nameId The name of the subtype
- * @param iconId The icon of the subtype
+ * Constructor.
+ * @param nameId Resource ID of the subtype name string. The string resource may have exactly
+ * one %s in it. If there is, the %s part will be replaced with the locale's display name by
+ * the formatter. Please refer to {@link #getDisplayName} for details.
+ * @param iconId Resource ID of the subtype icon drawable.
* @param locale The locale supported by the subtype
* @param mode The mode supported by the subtype
- * @param extraValue The extra value of the subtype
- * @param isAuxiliary true when this subtype is one shot subtype.
- * @param overridesImplicitlyEnabledSubtype true when this subtype should be selected by default
- * if no other subtypes are selected explicitly. Note that a subtype with this parameter being
- * true will not be shown in the subtypes list.
+ * @param extraValue The extra value of the subtype. This string is free-form, but the API
+ * supplies tools to deal with a key-value comma-separated list; see
+ * {@link #containsExtraValueKey} and {@link #getExtraValueOf}.
+ * @param isAuxiliary true when this subtype is auxiliary, false otherwise. An auxiliary
+ * subtype will not be shown in the list of enabled IMEs for choosing the current IME in
+ * the Settings even when this subtype is enabled. Please note that this subtype will still
+ * be shown in the list of IMEs in the IME switcher to allow the user to tentatively switch
+ * to this subtype while an IME is shown. The framework will never switch the current IME to
+ * this subtype by {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+ * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
+ * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
+ * @param overridesImplicitlyEnabledSubtype true when this subtype should be enabled by default
+ * if no other subtypes in the IME are enabled explicitly. Note that a subtype with this
+ * parameter being true will not be shown in the list of subtypes in each IME's subtype enabler.
+ * Having an "automatic" subtype is an example use of this flag.
*/
public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue,
boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype) {
@@ -115,52 +136,60 @@ public final class InputMethodSubtype implements Parcelable {
}
/**
- * @return the name of the subtype
+ * @return Resource ID of the subtype name string.
*/
public int getNameResId() {
return mSubtypeNameResId;
}
/**
- * @return the icon of the subtype
+ * @return Resource ID of the subtype icon drawable.
*/
public int getIconResId() {
return mSubtypeIconResId;
}
/**
- * @return the locale of the subtype
+ * @return The locale of the subtype. This method returns the "locale" string parameter passed
+ * to the constructor.
*/
public String getLocale() {
return mSubtypeLocale;
}
/**
- * @return the mode of the subtype
+ * @return The mode of the subtype.
*/
public String getMode() {
return mSubtypeMode;
}
/**
- * @return the extra value of the subtype
+ * @return The extra value of the subtype.
*/
public String getExtraValue() {
return mSubtypeExtraValue;
}
/**
- * @return true if this subtype is one shot subtype. One shot subtype will not be shown in the
- * ime switch list when this subtype is implicitly enabled. The framework will never
- * switch the current ime to this subtype by switchToLastInputMethod in InputMethodManager.
+ * @return true if this subtype is auxiliary, false otherwise. An auxiliary subtype will not be
+ * shown in the list of enabled IMEs for choosing the current IME in the Settings even when this
+ * subtype is enabled. Please note that this subtype will still be shown in the list of IMEs in
+ * the IME switcher to allow the user to tentatively switch to this subtype while an IME is
+ * shown. The framework will never switch the current IME to this subtype by
+ * {@link android.view.inputmethod.InputMethodManager#switchToLastInputMethod}.
+ * The intent of having this flag is to allow for IMEs that are invoked in a one-shot way as
+ * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice input).
*/
public boolean isAuxiliary() {
return mIsAuxiliary;
}
/**
- * @return true when this subtype is selected by default if no other subtypes are selected
- * explicitly. Note that a subtype that returns true will not be shown in the subtypes list.
+ * @return true when this subtype will be enabled by default if no other subtypes in the IME
+ * are enabled explicitly, false otherwise. Note that a subtype with this method returning true
+ * will not be shown in the list of subtypes in each IME's subtype enabler. Having an
+ * "automatic" subtype is an example use of this flag.
*/
public boolean overridesImplicitlyEnabledSubtype() {
return mOverridesImplicitlyEnabledSubtype;
@@ -171,10 +200,10 @@ public final class InputMethodSubtype implements Parcelable {
* @param packageName The package name of the IME
* @param appInfo The application info of the IME
* @return a display name for this subtype. The string resource of the label (mSubtypeNameResId)
- * can have only one %s in it. If there is, the %s part will be replaced with the locale's
- * display name by the formatter. If there is not, this method simply returns the string
- * specified by mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the
- * framework to generate an appropriate display name.
+ * may have exactly one %s in it. If there is, the %s part will be replaced with the locale's
+ * display name by the formatter. If there is not, this method returns the string specified by
+ * mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the framework to
+ * generate an appropriate display name.
*/
public CharSequence getDisplayName(
Context context, String packageName, ApplicationInfo appInfo) {
@@ -215,8 +244,8 @@ public final class InputMethodSubtype implements Parcelable {
/**
* The string of ExtraValue in subtype should be defined as follows:
* example: key0,key1=value1,key2,key3,key4=value4
- * @param key the key of extra value
- * @return the subtype contains specified the extra value
+ * @param key The key of extra value
+ * @return The subtype contains specified the extra value
*/
public boolean containsExtraValueKey(String key) {
return getExtraValueHashMap().containsKey(key);
@@ -225,8 +254,8 @@ public final class InputMethodSubtype implements Parcelable {
/**
* The string of ExtraValue in subtype should be defined as follows:
* example: key0,key1=value1,key2,key3,key4=value4
- * @param key the key of extra value
- * @return the value of the specified key
+ * @param key The key of extra value
+ * @return The value of the specified key
*/
public String getExtraValueOf(String key) {
return getExtraValueHashMap().get(key);
diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java
index ca6577f..f6418ce 100644
--- a/core/java/android/view/textservice/SpellCheckerSession.java
+++ b/core/java/android/view/textservice/SpellCheckerSession.java
@@ -91,14 +91,17 @@ public class SpellCheckerSession {
* This meta-data must reference an XML resource.
**/
public static final String SERVICE_META_DATA = "android.view.textservice.scs";
+ private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck";
private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
+ private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
private final InternalListener mInternalListener;
private final ITextServicesManager mTextServicesManager;
private final SpellCheckerInfo mSpellCheckerInfo;
private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
+ private final SpellCheckerSubtype mSubtype;
private boolean mIsUsed;
private SpellCheckerSessionListener mSpellCheckerSessionListener;
@@ -111,6 +114,9 @@ public class SpellCheckerSession {
case MSG_ON_GET_SUGGESTION_MULTIPLE:
handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
break;
+ case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
+ handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj);
+ break;
}
}
};
@@ -120,7 +126,8 @@ public class SpellCheckerSession {
* @hide
*/
public SpellCheckerSession(
- SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) {
+ SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener,
+ SpellCheckerSubtype subtype) {
if (info == null || listener == null || tsm == null) {
throw new NullPointerException();
}
@@ -130,6 +137,7 @@ public class SpellCheckerSession {
mTextServicesManager = tsm;
mIsUsed = true;
mSpellCheckerSessionListener = listener;
+ mSubtype = subtype;
}
/**
@@ -170,6 +178,14 @@ public class SpellCheckerSession {
}
/**
+ * @hide
+ */
+ public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) {
+ mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence(
+ new TextInfo[] {textInfo}, suggestionsLimit);
+ }
+
+ /**
* Get candidate strings for a substring of the specified text.
* @param textInfo text metadata for a spell checker
* @param suggestionsLimit the number of limit of suggestions returned
@@ -198,10 +214,15 @@ public class SpellCheckerSession {
mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
}
+ private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) {
+ mSpellCheckerSessionListener.onGetSuggestionsForSentence(suggestionInfos);
+ }
+
private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub {
private static final int TASK_CANCEL = 1;
private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
private static final int TASK_CLOSE = 3;
+ private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
private final Queue<SpellCheckerParams> mPendingTasks =
new LinkedList<SpellCheckerParams>();
private Handler mHandler;
@@ -256,6 +277,20 @@ public class SpellCheckerSession {
Log.e(TAG, "Failed to get suggestions " + e);
}
break;
+ case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
+ if (DBG) {
+ Log.w(TAG, "Get suggestions from the spell checker.");
+ }
+ if (scp.mTextInfos.length != 1) {
+ throw new IllegalArgumentException();
+ }
+ try {
+ session.onGetSuggestionsMultipleForSentence(
+ scp.mTextInfos, scp.mSuggestionsLimit);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get suggestions " + e);
+ }
+ break;
case TASK_CLOSE:
if (DBG) {
Log.w(TAG, "Close spell checker tasks.");
@@ -331,6 +366,15 @@ public class SpellCheckerSession {
suggestionsLimit, sequentialWords));
}
+ public void getSuggestionsMultipleForSentence(TextInfo[] textInfos, int suggestionsLimit) {
+ if (DBG) {
+ Log.w(TAG, "getSuggestionsMultipleForSentence");
+ }
+ processOrEnqueueTask(
+ new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
+ textInfos, suggestionsLimit, false));
+ }
+
public void close() {
if (DBG) {
Log.w(TAG, "close");
@@ -380,6 +424,12 @@ public class SpellCheckerSession {
}
}
}
+
+ @Override
+ public void onGetSuggestionsForSentence(SuggestionsInfo[] results) {
+ mHandler.sendMessage(
+ Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
+ }
}
/**
@@ -391,6 +441,10 @@ public class SpellCheckerSession {
* @param results an array of results of getSuggestions
*/
public void onGetSuggestions(SuggestionsInfo[] results);
+ /**
+ * @hide
+ */
+ public void onGetSuggestionsForSentence(SuggestionsInfo[] results);
}
private static class InternalListener extends ITextServicesSessionListener.Stub {
@@ -432,4 +486,11 @@ public class SpellCheckerSession {
public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
return mSpellCheckerSessionListenerImpl;
}
+
+ /**
+ * @hide
+ */
+ public boolean isSentenceSpellCheckSupported() {
+ return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK);
+ }
}
diff --git a/core/java/android/view/textservice/SpellCheckerSubtype.java b/core/java/android/view/textservice/SpellCheckerSubtype.java
index aeb3ba6..f235295 100644
--- a/core/java/android/view/textservice/SpellCheckerSubtype.java
+++ b/core/java/android/view/textservice/SpellCheckerSubtype.java
@@ -21,9 +21,11 @@ import android.content.pm.ApplicationInfo;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import android.util.Slog;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -33,11 +35,15 @@ import java.util.Locale;
* Subtype can describe locale (e.g. en_US, fr_FR...) used for settings.
*/
public final class SpellCheckerSubtype implements Parcelable {
+ private static final String TAG = SpellCheckerSubtype.class.getSimpleName();
+ private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
+ private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
private final int mSubtypeHashCode;
private final int mSubtypeNameResId;
private final String mSubtypeLocale;
private final String mSubtypeExtraValue;
+ private HashMap<String, String> mExtraValueHashMapCache;
/**
* Constructor
@@ -83,6 +89,46 @@ public final class SpellCheckerSubtype implements Parcelable {
return mSubtypeExtraValue;
}
+ private HashMap<String, String> getExtraValueHashMap() {
+ if (mExtraValueHashMapCache == null) {
+ mExtraValueHashMapCache = new HashMap<String, String>();
+ final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR);
+ final int N = pairs.length;
+ for (int i = 0; i < N; ++i) {
+ final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR);
+ if (pair.length == 1) {
+ mExtraValueHashMapCache.put(pair[0], null);
+ } else if (pair.length > 1) {
+ if (pair.length > 2) {
+ Slog.w(TAG, "ExtraValue has two or more '='s");
+ }
+ mExtraValueHashMapCache.put(pair[0], pair[1]);
+ }
+ }
+ }
+ return mExtraValueHashMapCache;
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key the key of extra value
+ * @return the subtype contains specified the extra value
+ */
+ public boolean containsExtraValueKey(String key) {
+ return getExtraValueHashMap().containsKey(key);
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key the key of extra value
+ * @return the value of the specified key
+ */
+ public String getExtraValueOf(String key) {
+ return getExtraValueHashMap().get(key);
+ }
+
@Override
public int hashCode() {
return mSubtypeHashCode;
diff --git a/core/java/android/view/textservice/SuggestionsInfo.java b/core/java/android/view/textservice/SuggestionsInfo.java
index ddd0361..9b99770 100644
--- a/core/java/android/view/textservice/SuggestionsInfo.java
+++ b/core/java/android/view/textservice/SuggestionsInfo.java
@@ -21,11 +21,14 @@ import com.android.internal.util.ArrayUtils;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.Arrays;
+
/**
* This class contains a metadata of suggestions from the text service
*/
public final class SuggestionsInfo implements Parcelable {
private static final String[] EMPTY = ArrayUtils.emptyArray(String.class);
+ private static final int NOT_A_LENGTH = -1;
/**
* Flag of the attributes of the suggestions that can be obtained by
@@ -47,6 +50,8 @@ public final class SuggestionsInfo implements Parcelable {
public static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = 0x0004;
private final int mSuggestionsAttributes;
private final String[] mSuggestions;
+ private final int[] mStartPosArray;
+ private final int[] mLengthArray;
private final boolean mSuggestionsAvailable;
private int mCookie;
private int mSequence;
@@ -57,16 +62,7 @@ public final class SuggestionsInfo implements Parcelable {
* @param suggestions from the text service
*/
public SuggestionsInfo(int suggestionsAttributes, String[] suggestions) {
- mSuggestionsAttributes = suggestionsAttributes;
- if (suggestions == null) {
- mSuggestions = EMPTY;
- mSuggestionsAvailable = false;
- } else {
- mSuggestions = suggestions;
- mSuggestionsAvailable = true;
- }
- mCookie = 0;
- mSequence = 0;
+ this(suggestionsAttributes, suggestions, 0, 0);
}
/**
@@ -78,12 +74,46 @@ public final class SuggestionsInfo implements Parcelable {
*/
public SuggestionsInfo(
int suggestionsAttributes, String[] suggestions, int cookie, int sequence) {
+ this(suggestionsAttributes, suggestions, cookie, sequence, null, null);
+ }
+
+ /**
+ * @hide
+ * Constructor.
+ * @param suggestionsAttributes from the text service
+ * @param suggestions from the text service
+ * @param cookie the cookie of the input TextInfo
+ * @param sequence the cookie of the input TextInfo
+ * @param startPosArray the array of start positions of suggestions
+ * @param lengthArray the array of length of suggestions
+ */
+ public SuggestionsInfo(
+ int suggestionsAttributes, String[] suggestions, int cookie, int sequence,
+ int[] startPosArray, int[] lengthArray) {
+ final int suggestsLen;
if (suggestions == null) {
mSuggestions = EMPTY;
mSuggestionsAvailable = false;
+ suggestsLen = 0;
+ mStartPosArray = new int[0];
+ mLengthArray = new int[0];
} else {
mSuggestions = suggestions;
mSuggestionsAvailable = true;
+ suggestsLen = suggestions.length;
+ if (startPosArray == null || lengthArray == null) {
+ mStartPosArray = new int[suggestsLen];
+ mLengthArray = new int[suggestsLen];
+ for (int i = 0; i < suggestsLen; ++i) {
+ mStartPosArray[i] = 0;
+ mLengthArray[i] = NOT_A_LENGTH;
+ }
+ } else if (suggestsLen != startPosArray.length || suggestsLen != lengthArray.length) {
+ throw new IllegalArgumentException();
+ } else {
+ mStartPosArray = Arrays.copyOf(startPosArray, suggestsLen);
+ mLengthArray = Arrays.copyOf(lengthArray, suggestsLen);
+ }
}
mSuggestionsAttributes = suggestionsAttributes;
mCookie = cookie;
@@ -96,6 +126,10 @@ public final class SuggestionsInfo implements Parcelable {
mCookie = source.readInt();
mSequence = source.readInt();
mSuggestionsAvailable = source.readInt() == 1;
+ mStartPosArray = new int[mSuggestions.length];
+ mLengthArray = new int[mSuggestions.length];
+ source.readIntArray(mStartPosArray);
+ source.readIntArray(mLengthArray);
}
/**
@@ -111,6 +145,8 @@ public final class SuggestionsInfo implements Parcelable {
dest.writeInt(mCookie);
dest.writeInt(mSequence);
dest.writeInt(mSuggestionsAvailable ? 1 : 0);
+ dest.writeIntArray(mStartPosArray);
+ dest.writeIntArray(mLengthArray);
}
/**
@@ -191,4 +227,24 @@ public final class SuggestionsInfo implements Parcelable {
public int describeContents() {
return 0;
}
+
+ /**
+ * @hide
+ */
+ public int getSuggestionStartPosAt(int i) {
+ if (i >= 0 && i < mStartPosArray.length) {
+ return mStartPosArray[i];
+ }
+ return -1;
+ }
+
+ /**
+ * @hide
+ */
+ public int getSuggestionLengthAt(int i) {
+ if (i >= 0 && i < mLengthArray.length) {
+ return mLengthArray[i];
+ }
+ return -1;
+ }
}
diff --git a/core/java/android/view/textservice/TextServicesManager.java b/core/java/android/view/textservice/TextServicesManager.java
index 69f88a5..fc59e6e 100644
--- a/core/java/android/view/textservice/TextServicesManager.java
+++ b/core/java/android/view/textservice/TextServicesManager.java
@@ -157,7 +157,8 @@ public final class TextServicesManager {
if (subtypeInUse == null) {
return null;
}
- final SpellCheckerSession session = new SpellCheckerSession(sci, sService, listener);
+ final SpellCheckerSession session = new SpellCheckerSession(
+ sci, sService, listener, subtypeInUse);
try {
sService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
session.getTextServicesSessionListener(),
diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java
index a810cf6..6fddb1a 100644
--- a/core/java/android/webkit/BrowserFrame.java
+++ b/core/java/android/webkit/BrowserFrame.java
@@ -410,6 +410,7 @@ class BrowserFrame extends Handler {
mCommitted = false;
// remove pending draw to block update until mFirstLayoutDone is
// set to true in didFirstLayout()
+ mWebViewCore.clearContent();
mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW);
}
}
diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java
index 5d54180..d7b6adb 100755
--- a/core/java/android/webkit/GeolocationPermissions.java
+++ b/core/java/android/webkit/GeolocationPermissions.java
@@ -27,30 +27,47 @@ import java.util.Vector;
/**
- * This class is used to get Geolocation permissions from, and set them on the
- * WebView. For example, it could be used to allow a user to manage Geolocation
- * permissions from a browser's UI.
+ * This class is used to manage permissions for the WebView's Geolocation
+ * JavaScript API.
*
- * Permissions are managed on a per-origin basis, as required by the
- * Geolocation spec - http://dev.w3.org/geo/api/spec-source.html. An origin
- * specifies the scheme, host and port of particular frame. An origin is
- * represented here as a string, using the output of
- * WebCore::SecurityOrigin::toString.
+ * Geolocation permissions are applied to an origin, which consists of the
+ * host, scheme and port of a URI. In order for web content to use the
+ * Geolocation API, permission must be granted for that content's origin.
*
- * This class is the Java counterpart of the WebKit C++ GeolocationPermissions
- * class. It simply marshalls calls from the UI thread to the WebKit thread.
+ * This class stores Geolocation permissions. An origin's permission state can
+ * be either allowed or denied. This class uses Strings to represent
+ * an origin.
*
- * Within WebKit, Geolocation permissions may be applied either temporarily
- * (for the duration of the page) or permanently. This class deals only with
- * permanent permissions.
+ * When an origin attempts to use the Geolocation API, but no permission state
+ * is currently set for that origin,
+ * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String,GeolocationPermissions.Callback) WebChromeClient.onGeolocationPermissionsShowPrompt()}
+ * is called. This allows the permission state to be set for that origin.
+ *
+ * The methods of this class can be used to modify and interrogate the stored
+ * Geolocation permissions at any time.
*/
+// This class is the Java counterpart of the WebKit C++ GeolocationPermissions
+// class. It simply marshalls calls from the UI thread to the WebKit thread.
+//
+// Within WebKit, Geolocation permissions may be applied either temporarily
+// (for the duration of the page) or permanently. This class deals only with
+// permanent permissions.
public final class GeolocationPermissions {
/**
- * Callback interface used by the browser to report a Geolocation permission
- * state set by the user in response to a permissions prompt.
+ * A callback interface used by the host application to set the Geolocation
+ * permission state for an origin.
*/
public interface Callback {
- public void invoke(String origin, boolean allow, boolean remember);
+ /**
+ * Set the Geolocation permission state for the supplied origin.
+ * @param origin The origin for which permissions are set.
+ * @param allow Whether or not the origin should be allowed to use the
+ * Geolocation API.
+ * @param retain Whether the permission should be retained beyond the
+ * lifetime of a page currently being displayed by a
+ * WebView.
+ */
+ public void invoke(String origin, boolean allow, boolean retain);
};
// Log tag
@@ -82,7 +99,8 @@ public final class GeolocationPermissions {
private static final String ALLOWED = "allowed";
/**
- * Gets the singleton instance of the class.
+ * Get the singleton instance of this class.
+ * @return The singleton {@link GeolocationPermissions} instance.
*/
public static GeolocationPermissions getInstance() {
if (sInstance == null) {
@@ -196,15 +214,18 @@ public final class GeolocationPermissions {
}
/**
- * Gets the set of origins for which Geolocation permissions are stored.
- * Note that we represent the origins as strings. These are created using
- * WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules'
- * (Database, Geolocation etc) do so, it's safe to match up origins based
- * on this string.
- *
- * Callback is a ValueCallback object whose onReceiveValue method will be
- * called asynchronously with the set of origins.
+ * Get the set of origins for which Geolocation permissions are stored.
+ * @param callback A {@link ValueCallback} to receive the result of this
+ * request. This object's
+ * {@link ValueCallback#onReceiveValue(T) onReceiveValue()}
+ * method will be invoked asynchronously with a set of
+ * Strings containing the origins for which Geolocation
+ * permissions are stored.
*/
+ // Note that we represent the origins as strings. These are created using
+ // WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules'
+ // (Database, Geolocation etc) do so, it's safe to match up origins based
+ // on this string.
public void getOrigins(ValueCallback<Set<String> > callback) {
if (callback != null) {
if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
@@ -217,10 +238,14 @@ public final class GeolocationPermissions {
}
/**
- * Gets the permission state for the specified origin.
- *
- * Callback is a ValueCallback object whose onReceiveValue method will be
- * called asynchronously with the permission state for the origin.
+ * Get the Geolocation permission state for the specified origin.
+ * @param origin The origin for which Geolocation permission is requested.
+ * @param callback A {@link ValueCallback} to receive the result of this
+ * request. This object's
+ * {@link ValueCallback#onReceiveValue(T) onReceiveValue()}
+ * method will be invoked asynchronously with a boolean
+ * indicating whether or not the origin can use the
+ * Geolocation API.
*/
public void getAllowed(String origin, ValueCallback<Boolean> callback) {
if (callback == null) {
@@ -242,27 +267,31 @@ public final class GeolocationPermissions {
}
/**
- * Clears the permission state for the specified origin. This method may be
- * called before the WebKit thread has intialized the message handler.
- * Messages will be queued until this time.
+ * Clear the Geolocation permission state for the specified origin.
+ * @param origin The origin for which Geolocation permissions are cleared.
*/
+ // This method may be called before the WebKit
+ // thread has intialized the message handler. Messages will be queued until
+ // this time.
public void clear(String origin) {
// Called on the UI thread.
postMessage(Message.obtain(null, CLEAR, origin));
}
/**
- * Allows the specified origin. This method may be called before the WebKit
- * thread has intialized the message handler. Messages will be queued until
- * this time.
+ * Allow the specified origin to use the Geolocation API.
+ * @param origin The origin for which Geolocation API use is allowed.
*/
+ // This method may be called before the WebKit
+ // thread has intialized the message handler. Messages will be queued until
+ // this time.
public void allow(String origin) {
// Called on the UI thread.
postMessage(Message.obtain(null, ALLOW, origin));
}
/**
- * Clears the permission state for all origins.
+ * Clear the Geolocation permission state for all origins.
*/
public void clearAll() {
// Called on the UI thread.
diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java
index e1eff58..bc0557e 100644
--- a/core/java/android/webkit/HTML5VideoFullScreen.java
+++ b/core/java/android/webkit/HTML5VideoFullScreen.java
@@ -116,13 +116,12 @@ public class HTML5VideoFullScreen extends HTML5VideoView
return mVideoSurfaceView;
}
- HTML5VideoFullScreen(Context context, int videoLayerId, int position,
- boolean autoStart) {
+ HTML5VideoFullScreen(Context context, int videoLayerId, int position) {
mVideoSurfaceView = new VideoSurfaceView(context);
mFullScreenMode = FULLSCREEN_OFF;
mVideoWidth = 0;
mVideoHeight = 0;
- init(videoLayerId, position, autoStart);
+ init(videoLayerId, position);
}
private void setMediaController(MediaController m) {
@@ -186,11 +185,6 @@ public class HTML5VideoFullScreen extends HTML5VideoView
// after reading the MetaData
if (mMediaController != null) {
mMediaController.setEnabled(true);
- // If paused , should show the controller for ever!
- if (getAutostart())
- mMediaController.show();
- else
- mMediaController.show(0);
}
if (mProgressView != null) {
@@ -201,6 +195,13 @@ public class HTML5VideoFullScreen extends HTML5VideoView
mVideoHeight = mp.getVideoHeight();
// This will trigger the onMeasure to get the display size right.
mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
+ // Call into the native to ask for the state, if still in play mode,
+ // this will trigger the video to play.
+ mProxy.dispatchOnRestoreState();
+
+ if (getStartWhenPrepared()) {
+ mPlayer.start();
+ }
}
public boolean fullScreenExited() {
diff --git a/core/java/android/webkit/HTML5VideoInline.java b/core/java/android/webkit/HTML5VideoInline.java
index fe5908e..2d5b263 100644
--- a/core/java/android/webkit/HTML5VideoInline.java
+++ b/core/java/android/webkit/HTML5VideoInline.java
@@ -34,9 +34,8 @@ public class HTML5VideoInline extends HTML5VideoView{
}
}
- HTML5VideoInline(int videoLayerId, int position,
- boolean autoStart) {
- init(videoLayerId, position, autoStart);
+ HTML5VideoInline(int videoLayerId, int position) {
+ init(videoLayerId, position);
mTextureNames = null;
}
diff --git a/core/java/android/webkit/HTML5VideoView.java b/core/java/android/webkit/HTML5VideoView.java
index 67660b8..73166cb 100644
--- a/core/java/android/webkit/HTML5VideoView.java
+++ b/core/java/android/webkit/HTML5VideoView.java
@@ -52,10 +52,6 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener {
// Switching between inline and full screen will also create a new instance.
protected MediaPlayer mPlayer;
- // This will be set up every time we create the Video View object.
- // Set to true only when switching into full screen while playing
- protected boolean mAutostart;
-
// We need to save such info.
protected Uri mUri;
protected Map<String, String> mHeaders;
@@ -141,22 +137,17 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener {
}
}
- public boolean getAutostart() {
- return mAutostart;
- }
-
public boolean getPauseDuringPreparing() {
return mPauseDuringPreparing;
}
// Every time we start a new Video, we create a VideoView and a MediaPlayer
- public void init(int videoLayerId, int position, boolean autoStart) {
+ public void init(int videoLayerId, int position) {
mPlayer = new MediaPlayer();
mCurrentState = STATE_INITIALIZED;
mProxy = null;
mVideoLayerId = videoLayerId;
mSaveSeekTime = position;
- mAutostart = autoStart;
mTimer = null;
mPauseDuringPreparing = false;
}
@@ -203,20 +194,9 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener {
mPlayer.setOnInfoListener(proxy);
}
- // Normally called immediately after setVideoURI. But for full screen,
- // this should be after surface holder created
- public void prepareDataAndDisplayMode(HTML5VideoViewProxy proxy) {
- // SurfaceTexture will be created lazily here for inline mode
- decideDisplayMode();
-
- setOnCompletionListener(proxy);
- setOnPreparedListener(proxy);
- setOnErrorListener(proxy);
- setOnInfoListener(proxy);
- // When there is exception, we could just bail out silently.
- // No Video will be played though. Write the stack for debug
+ public void prepareDataCommon(HTML5VideoViewProxy proxy) {
try {
- mPlayer.setDataSource(mProxy.getContext(), mUri, mHeaders);
+ mPlayer.setDataSource(proxy.getContext(), mUri, mHeaders);
mPlayer.prepareAsync();
} catch (IllegalArgumentException e) {
e.printStackTrace();
@@ -228,6 +208,25 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener {
mCurrentState = STATE_NOTPREPARED;
}
+ public void reprepareData(HTML5VideoViewProxy proxy) {
+ mPlayer.reset();
+ prepareDataCommon(proxy);
+ }
+
+ // Normally called immediately after setVideoURI. But for full screen,
+ // this should be after surface holder created
+ public void prepareDataAndDisplayMode(HTML5VideoViewProxy proxy) {
+ // SurfaceTexture will be created lazily here for inline mode
+ decideDisplayMode();
+
+ setOnCompletionListener(proxy);
+ setOnPreparedListener(proxy);
+ setOnErrorListener(proxy);
+ setOnInfoListener(proxy);
+
+ prepareDataCommon(proxy);
+ }
+
// Common code
public int getVideoLayerId() {
@@ -333,4 +332,14 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener {
return false;
}
+ private boolean m_startWhenPrepared = false;
+
+ public void setStartWhenPrepared(boolean willPlay) {
+ m_startWhenPrepared = willPlay;
+ }
+
+ public boolean getStartWhenPrepared() {
+ return m_startWhenPrepared;
+ }
+
}
diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java
index d0237b5..d306c86 100644
--- a/core/java/android/webkit/HTML5VideoViewProxy.java
+++ b/core/java/android/webkit/HTML5VideoViewProxy.java
@@ -66,6 +66,7 @@ class HTML5VideoViewProxy extends Handler
private static final int POSTER_FETCHED = 202;
private static final int PAUSED = 203;
private static final int STOPFULLSCREEN = 204;
+ private static final int RESTORESTATE = 205;
// Timer thread -> UI thread
private static final int TIMEUPDATE = 300;
@@ -144,19 +145,16 @@ class HTML5VideoViewProxy extends Handler
HTML5VideoViewProxy proxy, WebView webView) {
// Save the inline video info and inherit it in the full screen
int savePosition = 0;
- boolean savedIsPlaying = false;
if (mHTML5VideoView != null) {
// If we are playing the same video, then it is better to
// save the current position.
if (layerId == mHTML5VideoView.getVideoLayerId()) {
savePosition = mHTML5VideoView.getCurrentPosition();
- savedIsPlaying = mHTML5VideoView.isPlaying();
}
- mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
mHTML5VideoView.release();
}
mHTML5VideoView = new HTML5VideoFullScreen(proxy.getContext(),
- layerId, savePosition, savedIsPlaying);
+ layerId, savePosition);
mCurrentProxy = proxy;
mHTML5VideoView.setVideoURI(url, mCurrentProxy);
@@ -164,6 +162,16 @@ class HTML5VideoViewProxy extends Handler
mHTML5VideoView.enterFullScreenVideoState(layerId, proxy, webView);
}
+ public static void exitFullScreenVideo(HTML5VideoViewProxy proxy,
+ WebView webView) {
+ if (!mHTML5VideoView.fullScreenExited() && mHTML5VideoView.isFullScreenMode()) {
+ WebChromeClient client = webView.getWebChromeClient();
+ if (client != null) {
+ client.onHideCustomView();
+ }
+ }
+ }
+
// This is on the UI thread.
// When native tell Java to play, we need to check whether or not it is
// still the same video by using videoLayerId and treat it differently.
@@ -174,6 +182,21 @@ class HTML5VideoViewProxy extends Handler
if (mHTML5VideoView != null) {
currentVideoLayerId = mHTML5VideoView.getVideoLayerId();
backFromFullScreenMode = mHTML5VideoView.fullScreenExited();
+
+ // When playing video back to back in full screen mode,
+ // javascript will switch the src and call play.
+ // In this case, we can just reuse the same full screen view,
+ // and play the video after prepared.
+ if (mHTML5VideoView.isFullScreenMode()
+ && !backFromFullScreenMode
+ && currentVideoLayerId != videoLayerId
+ && mCurrentProxy != proxy) {
+ mCurrentProxy = proxy;
+ mHTML5VideoView.setStartWhenPrepared(true);
+ mHTML5VideoView.setVideoURI(url, proxy);
+ mHTML5VideoView.reprepareData(proxy);
+ return;
+ }
}
if (backFromFullScreenMode
@@ -192,7 +215,7 @@ class HTML5VideoViewProxy extends Handler
mHTML5VideoView.release();
}
mCurrentProxy = proxy;
- mHTML5VideoView = new HTML5VideoInline(videoLayerId, time, false);
+ mHTML5VideoView = new HTML5VideoInline(videoLayerId, time);
mHTML5VideoView.setVideoURI(url, mCurrentProxy);
mHTML5VideoView.prepareDataAndDisplayMode(proxy);
@@ -235,7 +258,7 @@ class HTML5VideoViewProxy extends Handler
}
public static void onPrepared() {
- if (!mHTML5VideoView.isFullScreenMode() || mHTML5VideoView.getAutostart()) {
+ if (!mHTML5VideoView.isFullScreenMode()) {
mHTML5VideoView.start();
}
if (mBaseLayer != 0) {
@@ -297,6 +320,11 @@ class HTML5VideoViewProxy extends Handler
mWebCoreHandler.sendMessage(msg);
}
+ public void dispatchOnRestoreState() {
+ Message msg = Message.obtain(mWebCoreHandler, RESTORESTATE);
+ mWebCoreHandler.sendMessage(msg);
+ }
+
public void onTimeupdate() {
sendMessage(obtainMessage(TIMEUPDATE));
}
@@ -569,6 +597,9 @@ class HTML5VideoViewProxy extends Handler
case STOPFULLSCREEN:
nativeOnStopFullscreen(mNativePointer);
break;
+ case RESTORESTATE:
+ nativeOnRestoreState(mNativePointer);
+ break;
}
}
};
@@ -676,6 +707,10 @@ class HTML5VideoViewProxy extends Handler
VideoPlayer.enterFullScreenVideo(layerId, url, this, mWebView);
}
+ public void exitFullScreenVideo() {
+ VideoPlayer.exitFullScreenVideo(this, mWebView);
+ }
+
/**
* The factory for HTML5VideoViewProxy instances.
* @param webViewCore is the WebViewCore that is requesting the proxy.
@@ -696,6 +731,7 @@ class HTML5VideoViewProxy extends Handler
private native void nativeOnPosterFetched(Bitmap poster, int nativePointer);
private native void nativeOnTimeupdate(int position, int nativePointer);
private native void nativeOnStopFullscreen(int nativePointer);
+ private native void nativeOnRestoreState(int nativePointer);
private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture,
int baseLayer, int videoLayerId, int textureName,
int playerState);
diff --git a/core/java/android/webkit/HttpAuthHandlerImpl.java b/core/java/android/webkit/HttpAuthHandlerImpl.java
index ac05125..01e8eb8 100644
--- a/core/java/android/webkit/HttpAuthHandlerImpl.java
+++ b/core/java/android/webkit/HttpAuthHandlerImpl.java
@@ -270,7 +270,6 @@ class HttpAuthHandlerImpl extends HttpAuthHandler {
/**
* Informs the WebView of a new set of credentials.
- * @hide Pending API council review
*/
public static void onReceivedCredentials(LoadListener loader,
String host, String realm, String username, String password) {
diff --git a/core/java/android/webkit/JniUtil.java b/core/java/android/webkit/JniUtil.java
index 4662040..7b44938 100644
--- a/core/java/android/webkit/JniUtil.java
+++ b/core/java/android/webkit/JniUtil.java
@@ -91,6 +91,16 @@ class JniUtil {
return sCacheDirectory;
}
+ /**
+ * Called by JNI. Gets the application's package name.
+ * @return String The application's package name
+ */
+ private static synchronized String getPackageName() {
+ checkInitialized();
+
+ return sContext.getPackageName();
+ }
+
private static final String ANDROID_CONTENT = "content:";
/**
diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java
index 30bbb04..ee9b949 100644
--- a/core/java/android/webkit/Network.java
+++ b/core/java/android/webkit/Network.java
@@ -169,7 +169,9 @@ class Network {
if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()))
return;
- NetworkInfo info = (NetworkInfo)intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ final ConnectivityManager connManager = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ final NetworkInfo info = connManager.getActiveNetworkInfo();
if (info != null)
mRoaming = info.isRoaming();
};
diff --git a/core/java/android/webkit/SearchBox.java b/core/java/android/webkit/SearchBox.java
index 6512c4b..38a1740 100644
--- a/core/java/android/webkit/SearchBox.java
+++ b/core/java/android/webkit/SearchBox.java
@@ -29,7 +29,7 @@ import java.util.List;
* SearchBox.query() and receive suggestions by registering a listener on the
* SearchBox object.
*
- * @hide pending API council approval.
+ * @hide
*/
public interface SearchBox {
/**
diff --git a/core/java/android/webkit/SelectActionModeCallback.java b/core/java/android/webkit/SelectActionModeCallback.java
index 8c174aa..cdf20f6 100644
--- a/core/java/android/webkit/SelectActionModeCallback.java
+++ b/core/java/android/webkit/SelectActionModeCallback.java
@@ -17,6 +17,7 @@
package android.webkit;
import android.app.SearchManager;
+import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.provider.Browser;
@@ -27,11 +28,16 @@ import android.view.MenuItem;
class SelectActionModeCallback implements ActionMode.Callback {
private WebView mWebView;
private ActionMode mActionMode;
+ private boolean mIsTextSelected = true;
void setWebView(WebView webView) {
mWebView = webView;
}
+ void setTextSelected(boolean isTextSelected) {
+ mIsTextSelected = isTextSelected;
+ }
+
void finish() {
// It is possible that onCreateActionMode was never called, in the case
// where there is no ActionBar, for example.
@@ -52,17 +58,25 @@ class SelectActionModeCallback implements ActionMode.Callback {
mode.setTitle(allowText ?
context.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
- if (!mode.isUiFocusable()) {
- // If the action mode UI we're running in isn't capable of taking window focus
- // the user won't be able to type into the find on page UI. Disable this functionality.
- // (Note that this should only happen in floating dialog windows.)
- // This can be removed once we can handle multiple focusable windows at a time
- // in a better way.
- final MenuItem findOnPageItem = menu.findItem(com.android.internal.R.id.find);
- if (findOnPageItem != null) {
- findOnPageItem.setVisible(false);
- }
- }
+ // If the action mode UI we're running in isn't capable of taking window focus
+ // the user won't be able to type into the find on page UI. Disable this functionality.
+ // (Note that this should only happen in floating dialog windows.)
+ // This can be removed once we can handle multiple focusable windows at a time
+ // in a better way.
+ ClipboardManager cm = (ClipboardManager)(context
+ .getSystemService(Context.CLIPBOARD_SERVICE));
+ boolean isFocusable = mode.isUiFocusable();
+ boolean isEditable = mWebView.focusCandidateIsEditableText();
+ boolean canPaste = isEditable && cm.hasPrimaryClip() && isFocusable;
+ boolean canFind = !isEditable && isFocusable;
+ boolean canCut = isEditable && mIsTextSelected && isFocusable;
+ boolean canCopy = mIsTextSelected;
+ boolean canWebSearch = mIsTextSelected;
+ setMenuVisibility(menu, canFind, com.android.internal.R.id.find);
+ setMenuVisibility(menu, canPaste, com.android.internal.R.id.paste);
+ setMenuVisibility(menu, canCut, com.android.internal.R.id.cut);
+ setMenuVisibility(menu, canCopy, com.android.internal.R.id.copy);
+ setMenuVisibility(menu, canWebSearch, com.android.internal.R.id.websearch);
mActionMode = mode;
return true;
}
@@ -75,11 +89,21 @@ class SelectActionModeCallback implements ActionMode.Callback {
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId()) {
+ case android.R.id.cut:
+ mWebView.cutSelection();
+ mode.finish();
+ break;
+
case android.R.id.copy:
mWebView.copySelection();
mode.finish();
break;
+ case android.R.id.paste:
+ mWebView.pasteFromClipboard();
+ mode.finish();
+ break;
+
case com.android.internal.R.id.share:
String selection = mWebView.getSelection();
Browser.sendString(mWebView.getContext(), selection);
@@ -113,4 +137,11 @@ class SelectActionModeCallback implements ActionMode.Callback {
public void onDestroyActionMode(ActionMode mode) {
mWebView.selectionDone();
}
+
+ private void setMenuVisibility(Menu menu, boolean visible, int resourceId) {
+ final MenuItem item = menu.findItem(resourceId);
+ if (item != null) {
+ item.setVisible(visible);
+ }
+ }
}
diff --git a/core/java/android/webkit/ValueCallback.java b/core/java/android/webkit/ValueCallback.java
index 1a167e8..5c7d97f 100644
--- a/core/java/android/webkit/ValueCallback.java
+++ b/core/java/android/webkit/ValueCallback.java
@@ -17,11 +17,12 @@
package android.webkit;
/**
- * A callback interface used to returns values asynchronously
+ * A callback interface used to provide values asynchronously.
*/
public interface ValueCallback<T> {
/**
- * Invoked when we have the result
+ * Invoked when the value is available.
+ * @param value The value.
*/
public void onReceiveValue(T value);
};
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index 3d129f7..a6ef0ce 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -250,14 +250,24 @@ public class WebChromeClient {
}
/**
- * Instructs the client to show a prompt to ask the user to set the
- * Geolocation permission state for the specified origin.
+ * Notify the host application that web content from the specified origin
+ * is attempting to use the Geolocation API, but no permission state is
+ * currently set for that origin. The host application should invoke the
+ * specified callback with the desired permission state. See
+ * {@link GeolocationPermissions} for details.
+ * @param origin The origin of the web content attempting to use the
+ * Geolocation API.
+ * @param callback The callback to use to set the permission state for the
+ * origin.
*/
public void onGeolocationPermissionsShowPrompt(String origin,
GeolocationPermissions.Callback callback) {}
/**
- * Instructs the client to hide the Geolocation permissions prompt.
+ * Notify the host application that a request for Geolocation permissions,
+ * made with a previous call to
+ * {@link #onGeolocationPermissionsShowPrompt(String,GeolocationPermissions.Callback) onGeolocationPermissionsShowPrompt()}
+ * has been canceled. Any related UI should therefore be hidden.
*/
public void onGeolocationPermissionsHidePrompt() {}
diff --git a/core/java/android/webkit/WebCoreThreadWatchdog.java b/core/java/android/webkit/WebCoreThreadWatchdog.java
new file mode 100644
index 0000000..d100260
--- /dev/null
+++ b/core/java/android/webkit/WebCoreThreadWatchdog.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.webkit.WebViewCore.EventHub;
+
+// A Runnable that will monitor if the WebCore thread is still
+// processing messages by pinging it every so often. It is safe
+// to call the public methods of this class from any thread.
+class WebCoreThreadWatchdog implements Runnable {
+
+ // A message with this id is sent by the WebCore thread to notify the
+ // Watchdog that the WebCore thread is still processing messages
+ // (i.e. everything is OK).
+ private static final int IS_ALIVE = 100;
+
+ // This message is placed in the Watchdog's queue and removed when we
+ // receive an IS_ALIVE. If it is ever processed, we consider the
+ // WebCore thread unresponsive.
+ private static final int TIMED_OUT = 101;
+
+ // Message to tell the Watchdog thread to terminate.
+ private static final int QUIT = 102;
+
+ // Wait 10s after hearing back from the WebCore thread before checking it's still alive.
+ private static final int HEARTBEAT_PERIOD = 10 * 1000;
+
+ // If there's no callback from the WebCore thread for 30s, prompt the user the page has
+ // become unresponsive.
+ private static final int TIMEOUT_PERIOD = 30 * 1000;
+
+ // After the first timeout, use a shorter period before re-prompting the user.
+ private static final int SUBSEQUENT_TIMEOUT_PERIOD = 15 * 1000;
+
+ private Context mContext;
+ private Handler mWebCoreThreadHandler;
+ private Handler mHandler;
+ private boolean mPaused;
+ private boolean mPendingQuit;
+
+ private static WebCoreThreadWatchdog sInstance;
+
+ public synchronized static WebCoreThreadWatchdog start(Context context,
+ Handler webCoreThreadHandler) {
+ if (sInstance == null) {
+ sInstance = new WebCoreThreadWatchdog(context, webCoreThreadHandler);
+ new Thread(sInstance, "WebCoreThreadWatchdog").start();
+ }
+ return sInstance;
+ }
+
+ public synchronized static void updateContext(Context context) {
+ if (sInstance != null) {
+ sInstance.setContext(context);
+ }
+ }
+
+ public synchronized static void pause() {
+ if (sInstance != null) {
+ sInstance.pauseWatchdog();
+ }
+ }
+
+ public synchronized static void resume() {
+ if (sInstance != null) {
+ sInstance.resumeWatchdog();
+ }
+ }
+
+ public synchronized static void quit() {
+ if (sInstance != null) {
+ sInstance.quitWatchdog();
+ }
+ }
+
+ private void setContext(Context context) {
+ mContext = context;
+ }
+
+ private WebCoreThreadWatchdog(Context context, Handler webCoreThreadHandler) {
+ mContext = context;
+ mWebCoreThreadHandler = webCoreThreadHandler;
+ }
+
+ private void quitWatchdog() {
+ if (mHandler == null) {
+ // The thread hasn't started yet, so set a flag to stop it starting.
+ mPendingQuit = true;
+ return;
+ }
+ // Clear any pending messages, and then post a quit to the WatchDog handler.
+ mHandler.removeMessages(TIMED_OUT);
+ mHandler.removeMessages(IS_ALIVE);
+ mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT);
+ mHandler.obtainMessage(QUIT).sendToTarget();
+ }
+
+ private void pauseWatchdog() {
+ mPaused = true;
+
+ if (mHandler == null) {
+ return;
+ }
+
+ mHandler.removeMessages(TIMED_OUT);
+ mHandler.removeMessages(IS_ALIVE);
+ mWebCoreThreadHandler.removeMessages(EventHub.HEARTBEAT);
+ }
+
+ private void resumeWatchdog() {
+ if (!mPaused) {
+ // Do nothing if we get a call to resume without being paused.
+ // This can happen during the initialisation of the WebView.
+ return;
+ }
+
+ mPaused = false;
+
+ if (mHandler == null) {
+ return;
+ }
+
+ mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
+ mHandler.obtainMessage(IS_ALIVE)).sendToTarget();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
+ }
+
+ private boolean createHandler() {
+ synchronized (WebCoreThreadWatchdog.class) {
+ if (mPendingQuit) {
+ return false;
+ }
+
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case IS_ALIVE:
+ synchronized(WebCoreThreadWatchdog.class) {
+ if (mPaused) {
+ return;
+ }
+
+ // The WebCore thread still seems alive. Reset the countdown timer.
+ removeMessages(TIMED_OUT);
+ sendMessageDelayed(obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
+ mWebCoreThreadHandler.sendMessageDelayed(
+ mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
+ mHandler.obtainMessage(IS_ALIVE)),
+ HEARTBEAT_PERIOD);
+ }
+ break;
+
+ case TIMED_OUT:
+ new AlertDialog.Builder(mContext)
+ .setMessage(com.android.internal.R.string.webpage_unresponsive)
+ .setPositiveButton(com.android.internal.R.string.force_close,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // User chose to force close.
+ Process.killProcess(Process.myPid());
+ }
+ })
+ .setNegativeButton(com.android.internal.R.string.wait,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // The user chose to wait. The last HEARTBEAT message
+ // will still be in the WebCore thread's queue, so all
+ // we need to do is post another TIMED_OUT so that the
+ // user will get prompted again if the WebCore thread
+ // doesn't sort itself out.
+ sendMessageDelayed(obtainMessage(TIMED_OUT),
+ SUBSEQUENT_TIMEOUT_PERIOD);
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ sendMessageDelayed(obtainMessage(TIMED_OUT),
+ SUBSEQUENT_TIMEOUT_PERIOD);
+ }
+ })
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .show();
+ break;
+
+ case QUIT:
+ Looper.myLooper().quit();
+ break;
+ }
+ }
+ };
+
+ return true;
+ }
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+
+ if (!createHandler()) {
+ return;
+ }
+
+ // Send the initial control to WebViewCore and start the timeout timer as long as we aren't
+ // paused.
+ synchronized (WebCoreThreadWatchdog.class) {
+ if (!mPaused) {
+ mWebCoreThreadHandler.obtainMessage(EventHub.HEARTBEAT,
+ mHandler.obtainMessage(IS_ALIVE)).sendToTarget();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMED_OUT), TIMEOUT_PERIOD);
+ }
+ }
+
+ Looper.loop();
+ }
+}
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index f947f95..b4c38db 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -142,7 +142,7 @@ public class WebSettings {
}
// TODO: Keep this up to date
- private static final String PREVIOUS_VERSION = "3.1";
+ private static final String PREVIOUS_VERSION = "4.0.3";
// WebView associated with this WebSettings.
private WebView mWebView;
@@ -201,7 +201,7 @@ public class WebSettings {
private boolean mXSSAuditorEnabled = false;
// HTML5 configuration parameters
private long mAppCacheMaxSize = Long.MAX_VALUE;
- private String mAppCachePath = "";
+ private String mAppCachePath = null;
private String mDatabasePath = "";
// The WebCore DatabaseTracker only allows the database path to be set
// once. Keep track of when the path has been set.
@@ -535,21 +535,6 @@ 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) {
@@ -1370,8 +1355,8 @@ public class WebSettings {
}
/**
- * Tell the WebView to enable Application Caches API.
- * @param flag True if the WebView should enable Application Caches.
+ * Enable or disable the Application Cache API.
+ * @param flag Whether to enable the Application Cache API.
*/
public synchronized void setAppCacheEnabled(boolean flag) {
if (mAppCacheEnabled != flag) {
@@ -1381,15 +1366,19 @@ public class WebSettings {
}
/**
- * Set a custom path to the Application Caches files. The client
- * must ensure it exists before this call.
- * @param appCachePath String path to the directory containing Application
- * Caches files. The appCache path can be the empty string but should not
- * be null. Passing null for this parameter will result in a no-op.
+ * Set the path used by the Application Cache API to store files. This
+ * setting is applied to all WebViews in the application. In order for the
+ * Application Cache API to function, this method must be called with a
+ * path which exists and is writable by the application. This method may
+ * only be called once: repeated calls are ignored.
+ * @param path Path to the directory that should be used to store Application
+ * Cache files.
*/
- public synchronized void setAppCachePath(String appCachePath) {
- if (appCachePath != null && !appCachePath.equals(mAppCachePath)) {
- mAppCachePath = appCachePath;
+ public synchronized void setAppCachePath(String path) {
+ // We test for a valid path and for repeated setting on the native
+ // side, but we can avoid syncing in some simple cases.
+ if (mAppCachePath == null && path != null && !path.isEmpty()) {
+ mAppCachePath = path;
postSync();
}
}
@@ -1459,7 +1448,7 @@ public class WebSettings {
* @param flag True if the WebView should enable WebWorkers.
* Note that this flag only affects V8. JSC does not have
* an equivalent setting.
- * @hide pending api council approval
+ * @hide
*/
public synchronized void setWorkersEnabled(boolean flag) {
if (mWorkersEnabled != flag) {
@@ -1710,7 +1699,7 @@ 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
+ * @hide
*/
public void setMaximumDecodedImageSize(long size) {
if (mMaximumDecodedImageSize != size) {
diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java
index 8eb1524..c079404 100644
--- a/core/java/android/webkit/WebStorage.java
+++ b/core/java/android/webkit/WebStorage.java
@@ -362,7 +362,7 @@ public final class WebStorage {
/**
* Sets the maximum size of the ApplicationCache.
* This should only ever be called on the WebKit thread.
- * @hide Pending API council approval
+ * @hide
*/
public void setAppCacheMaximumSize(long size) {
nativeSetAppCacheMaximumSize(size);
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 24eebd7..cc8eef2 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -20,6 +20,7 @@ import android.annotation.Widget;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
+import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
@@ -27,7 +28,6 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.database.DataSetObserver;
@@ -57,8 +57,13 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.StrictMode;
+import android.os.SystemClock;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.EventLog;
import android.util.Log;
@@ -83,6 +88,7 @@ import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
@@ -91,6 +97,7 @@ import android.webkit.WebViewCore.DrawData;
import android.webkit.WebViewCore.EventHub;
import android.webkit.WebViewCore.TouchEventData;
import android.webkit.WebViewCore.TouchHighlightData;
+import android.webkit.WebViewCore.WebKitHitTest;
import android.widget.AbsoluteLayout;
import android.widget.Adapter;
import android.widget.AdapterView;
@@ -122,11 +129,6 @@ import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import javax.microedition.khronos.egl.EGL10;
-import javax.microedition.khronos.egl.EGLContext;
-import javax.microedition.khronos.egl.EGLDisplay;
-import static javax.microedition.khronos.egl.EGL10.*;
-
/**
* <p>A View that displays web pages. This class is the basis upon which you
* can roll your own web browser or simply display some online content within your Activity.
@@ -206,10 +208,10 @@ import static javax.microedition.khronos.egl.EGL10.*;
* <li>Modifying the {@link android.webkit.WebSettings}, such as
* enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean)
* setJavaScriptEnabled()}. </li>
- * <li>Adding JavaScript-to-Java interfaces with the {@link
- * android.webkit.WebView#addJavascriptInterface} method.
- * This lets you bind Java objects into the WebView so they can be
- * controlled from the web pages JavaScript.</li>
+ * <li>Injecting Java objects into the WebView using the
+ * {@link android.webkit.WebView#addJavascriptInterface} method. This
+ * method allows you to inject Java objects into a page's JavaScript
+ * context, so that they can be accessed by JavaScript in the page.</li>
* </ul>
*
* <p>Here's a more complicated example, showing error handling,
@@ -324,6 +326,15 @@ import static javax.microedition.khronos.egl.EGL10.*;
* property to {@code device-dpi}. This stops Android from performing scaling in your web page and
* allows you to make the necessary adjustments for each density via CSS and JavaScript.</p>
*
+ * <h3>HTML5 Video support</h3>
+ *
+ * <p>In order to support inline HTML5 video in your application, you need to have hardware
+ * acceleration turned on, and set a {@link android.webkit.WebChromeClient}. For full screen support,
+ * implementations of {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)}
+ * and {@link WebChromeClient#onHideCustomView()} are required,
+ * {@link WebChromeClient#getVideoLoadingProgressView()} is optional.
+ * </p>
+ *
*
*/
@Widget
@@ -332,6 +343,7 @@ public class WebView extends AbsoluteLayout
ViewGroup.OnHierarchyChangeListener {
private class InnerGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
+ @Override
public void onGlobalLayout() {
if (isShown()) {
setGLRectViewport();
@@ -340,6 +352,7 @@ public class WebView extends AbsoluteLayout
}
private class InnerScrollChangedListener implements ViewTreeObserver.OnScrollChangedListener {
+ @Override
public void onScrollChanged() {
if (isShown()) {
setGLRectViewport();
@@ -347,6 +360,130 @@ public class WebView extends AbsoluteLayout
}
}
+ /**
+ * InputConnection used for ContentEditable. This captures changes
+ * to the text and sends them either as key strokes or text changes.
+ */
+ private class WebViewInputConnection extends BaseInputConnection {
+ // Used for mapping characters to keys typed.
+ private KeyCharacterMap mKeyCharacterMap;
+
+ public WebViewInputConnection() {
+ super(WebView.this, true);
+ }
+
+ @Override
+ public boolean setComposingText(CharSequence text, int newCursorPosition) {
+ Editable editable = getEditable();
+ int start = getComposingSpanStart(editable);
+ int end = getComposingSpanEnd(editable);
+ if (start < 0 || end < 0) {
+ start = Selection.getSelectionStart(editable);
+ end = Selection.getSelectionEnd(editable);
+ }
+ if (end < start) {
+ int temp = end;
+ end = start;
+ start = temp;
+ }
+ setNewText(start, end, text);
+ return super.setComposingText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ setComposingText(text, newCursorPosition);
+ finishComposingText();
+ return true;
+ }
+
+ @Override
+ public boolean deleteSurroundingText(int leftLength, int rightLength) {
+ Editable editable = getEditable();
+ int cursorPosition = Selection.getSelectionEnd(editable);
+ int startDelete = Math.max(0, cursorPosition - leftLength);
+ int endDelete = Math.min(editable.length(),
+ cursorPosition + rightLength);
+ setNewText(startDelete, endDelete, "");
+ return super.deleteSurroundingText(leftLength, rightLength);
+ }
+
+ /**
+ * Sends a text change to webkit indirectly. If it is a single-
+ * character add or delete, it sends it as a key stroke. If it cannot
+ * be represented as a key stroke, it sends it as a field change.
+ * @param start The start offset (inclusive) of the text being changed.
+ * @param end The end offset (exclusive) of the text being changed.
+ * @param text The new text to replace the changed text.
+ */
+ private void setNewText(int start, int end, CharSequence text) {
+ Editable editable = getEditable();
+ CharSequence original = editable.subSequence(start, end);
+ boolean isCharacterAdd = false;
+ boolean isCharacterDelete = false;
+ int textLength = text.length();
+ int originalLength = original.length();
+ if (textLength > originalLength) {
+ isCharacterAdd = (textLength == originalLength + 1)
+ && TextUtils.regionMatches(text, 0, original, 0,
+ originalLength);
+ } else if (originalLength > textLength) {
+ isCharacterDelete = (textLength == originalLength - 1)
+ && TextUtils.regionMatches(text, 0, original, 0,
+ textLength);
+ }
+ if (isCharacterAdd) {
+ sendCharacter(text.charAt(textLength - 1));
+ mTextGeneration++;
+ } else if (isCharacterDelete) {
+ sendDeleteKey();
+ mTextGeneration++;
+ } else if (textLength != originalLength ||
+ !TextUtils.regionMatches(text, 0, original, 0,
+ textLength)) {
+ // Send a message so that key strokes and text replacement
+ // do not come out of order.
+ Message replaceMessage = mPrivateHandler.obtainMessage(
+ REPLACE_TEXT, start, end, text.toString());
+ mPrivateHandler.sendMessage(replaceMessage);
+ }
+ }
+
+ /**
+ * Send a single character to the WebView as a key down and up event.
+ * @param c The character to be sent.
+ */
+ private void sendCharacter(char c) {
+ if (mKeyCharacterMap == null) {
+ mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ }
+ char[] chars = new char[1];
+ chars[0] = c;
+ KeyEvent[] events = mKeyCharacterMap.getEvents(chars);
+ if (events != null) {
+ for (KeyEvent event : events) {
+ sendKeyEvent(event);
+ }
+ }
+ }
+
+ /**
+ * Send the delete character as a key down and up event.
+ */
+ private void sendDeleteKey() {
+ long eventTime = SystemClock.uptimeMillis();
+ sendKeyEvent(new KeyEvent(eventTime, eventTime,
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD));
+ sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0, 0,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD));
+ }
+ }
+
+
// The listener to capture global layout change event.
private InnerGlobalLayoutListener mGlobalLayoutListener = null;
@@ -371,6 +508,8 @@ public class WebView extends AbsoluteLayout
private final Rect mViewRectViewport = new Rect();
private final RectF mVisibleContentRect = new RectF();
private boolean mGLViewportEmpty = false;
+ WebViewInputConnection mInputConnection = null;
+
/**
* Transportation object for returning WebView across thread boundaries.
@@ -567,6 +706,7 @@ public class WebView extends AbsoluteLayout
private boolean mIsPaused;
private HitTestResult mInitialHitTestResult;
+ private WebKitHitTest mFocusedNode;
/**
* Customizable constant
@@ -656,15 +796,13 @@ public class WebView extends AbsoluteLayout
private Drawable mSelectHandleLeft;
private Drawable mSelectHandleRight;
- static final boolean USE_WEBKIT_RINGS = false;
+ static boolean sDisableNavcache = false;
// the color used to highlight the touch rectangles
- private static final int HIGHLIGHT_COLOR = 0x6633b5e5;
- // the round corner for the highlight path
- private static final float TOUCH_HIGHLIGHT_ARC = 5.0f;
+ static final int HIGHLIGHT_COLOR = 0x6633b5e5;
// the region indicating where the user touched on the screen
private Region mTouchHighlightRegion = new Region();
// the paint for the touch highlight
- private Paint mTouchHightlightPaint;
+ private Paint mTouchHightlightPaint = new Paint();
// debug only
private static final boolean DEBUG_TOUCH_HIGHLIGHT = true;
private static final int TOUCH_HIGHLIGHT_ELAPSE_TIME = 2000;
@@ -732,7 +870,7 @@ public class WebView extends AbsoluteLayout
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 HIT_TEST_RESULT = 131;
static final int SAVE_WEBARCHIVE_FINISHED = 132;
static final int SET_AUTOFILLABLE = 133;
@@ -743,9 +881,14 @@ public class WebView extends AbsoluteLayout
static final int ENTER_FULLSCREEN_VIDEO = 137;
static final int UPDATE_SELECTION = 138;
static final int UPDATE_ZOOM_DENSITY = 139;
+ static final int EXIT_FULLSCREEN_VIDEO = 140;
+
+ static final int COPY_TO_CLIPBOARD = 141;
+ static final int INIT_EDIT_FIELD = 142;
+ static final int REPLACE_TEXT = 143;
private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID;
- private static final int LAST_PACKAGE_MSG_ID = SET_TOUCH_HIGHLIGHT_RECTS;
+ private static final int LAST_PACKAGE_MSG_ID = HIT_TEST_RESULT;
static final String[] HandlerPrivateDebugString = {
"REMEMBER_PASSWORD", // = 1;
@@ -849,13 +992,12 @@ public class WebView extends AbsoluteLayout
// 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 =
+ // Template for JavaScript that injects a screen-reader.
+ private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE =
"javascript:(function() {" +
" var chooser = document.createElement('script');" +
" chooser.type = 'text/javascript';" +
- " chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" +
+ " chooser.src = '%1s';" +
" document.getElementsByTagName('head')[0].appendChild(chooser);" +
" })();";
@@ -921,9 +1063,6 @@ public class WebView extends AbsoluteLayout
private Rect mScrollingLayerBounds = new Rect();
private boolean mSentAutoScrollMessage = false;
- // Temporary hack to work around the context removal upon memory pressure
- private static boolean mIncrementEGLContextHack = false;
-
// used for serializing asynchronously handled touch events.
private final TouchEventQueue mTouchEventQueue = new TouchEventQueue();
@@ -952,8 +1091,7 @@ public class WebView extends AbsoluteLayout
public void onNewPicture(WebView view, Picture picture);
}
- // FIXME: Want to make this public, but need to change the API file.
- public /*static*/ class HitTestResult {
+ public static class HitTestResult {
/**
* Default HitTestResult, where the target is unknown
*/
@@ -1012,16 +1150,34 @@ public class WebView extends AbsoluteLayout
mExtra = extra;
}
+ /**
+ * Gets the type of the hit test result.
+ * @return See the XXX_TYPE constants defined in this class.
+ */
public int getType() {
return mType;
}
+ /**
+ * Gets additional type-dependant information about the result, see
+ * {@link WebView#getHitTestResult()} for details.
+ * @return may either be null or contain extra information about this result.
+ */
public String getExtra() {
return mExtra;
}
}
/**
+ * Refer to {@link WebView#requestFocusNodeHref(Message)} for more information
+ */
+ static class FocusNodeHref {
+ static final String TITLE = "title";
+ static final String URL = "url";
+ static final String SRC = "src";
+ }
+
+ /**
* Construct a new WebView with a Context object.
* @param context A Context object used to access application assets.
*/
@@ -1053,6 +1209,7 @@ 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 privateBrowsing If true the web view will be initialized in private mode.
*/
public WebView(Context context, AttributeSet attrs, int defStyle,
boolean privateBrowsing) {
@@ -1069,7 +1226,8 @@ public class WebView extends AbsoluteLayout
* @param defStyle The default style resource ID.
* @param javaScriptInterfaces is a Map of interface names, as keys, and
* object implementing those interfaces, as values.
- * @hide pending API council approval.
+ * @param privateBrowsing If true the web view will be initialized in private mode.
+ * @hide This is an implementation detail.
*/
protected WebView(Context context, AttributeSet attrs, int defStyle,
Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
@@ -1232,7 +1390,7 @@ public class WebView extends AbsoluteLayout
PackageManager pm = mContext.getPackageManager();
for (String name : sGoogleApps) {
try {
- PackageInfo pInfo = pm.getPackageInfo(name,
+ pm.getPackageInfo(name,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
installedPackages.add(name);
} catch (PackageManager.NameNotFoundException e) {
@@ -1259,6 +1417,7 @@ public class WebView extends AbsoluteLayout
private void init() {
OnTrimMemoryListener.init(getContext());
+ sDisableNavcache = nativeDisableNavcache();
setWillNotDraw(false);
setFocusable(true);
@@ -1311,7 +1470,7 @@ public class WebView extends AbsoluteLayout
final String packageName = ctx.getPackageName();
if (packageName != null) {
mTextToSpeech = new TextToSpeech(getContext(), null, null,
- packageName + ".**webview**");
+ packageName + ".**webview**", true);
addJavascriptInterface(mTextToSpeech, ALIAS_ACCESSIBILITY_JS_INTERFACE);
}
}
@@ -1404,23 +1563,27 @@ public class WebView extends AbsoluteLayout
.setMessage(com.android.internal.R.string.save_password_message)
.setPositiveButton(com.android.internal.R.string.save_password_notnow,
new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int which) {
resumeMsg.sendToTarget();
}
})
.setNeutralButton(com.android.internal.R.string.save_password_remember,
new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int which) {
remember.sendToTarget();
}
})
.setNegativeButton(com.android.internal.R.string.save_password_never,
new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int which) {
neverRemember.sendToTarget();
}
})
.setOnCancelListener(new OnCancelListener() {
+ @Override
public void onCancel(DialogInterface dialog) {
resumeMsg.sendToTarget();
}
@@ -1506,6 +1669,7 @@ public class WebView extends AbsoluteLayout
*
* @deprecated This method is now obsolete.
*/
+ @Deprecated
public int getVisibleTitleHeight() {
checkThread();
return getVisibleTitleHeightImpl();
@@ -1734,7 +1898,7 @@ public class WebView extends AbsoluteLayout
*
* @param flags JS engine flags in a String
*
- * @hide pending API solidification
+ * @hide This is an implementation detail.
*/
public void setJsFlags(String flags) {
checkThread();
@@ -1844,6 +2008,7 @@ public class WebView extends AbsoluteLayout
// contains valid data.
final File temp = new File(dest.getPath() + ".writing");
new Thread(new Runnable() {
+ @Override
public void run() {
FileOutputStream out = null;
try {
@@ -1913,6 +2078,7 @@ public class WebView extends AbsoluteLayout
final FileInputStream in = new FileInputStream(src);
final Bundle copy = new Bundle(b);
new Thread(new Runnable() {
+ @Override
public void run() {
try {
final Picture p = Picture.createFromStream(in);
@@ -1920,6 +2086,7 @@ public class WebView extends AbsoluteLayout
// Post a runnable on the main thread to update the
// history picture fields.
mPrivateHandler.post(new Runnable() {
+ @Override
public void run() {
restoreHistoryPictureFields(p, copy);
}
@@ -2496,11 +2663,12 @@ public class WebView extends AbsoluteLayout
}
/**
- * Return the reading level scale of the WebView
+ * Compute the reading level scale of the WebView
+ * @param scale The current scale.
* @return The reading level scale.
*/
- /*package*/ float getReadingLevelScale() {
- return mZoomManager.getReadingLevelScale();
+ /*package*/ float computeReadingLevelScale(float scale) {
+ return mZoomManager.computeReadingLevelScale(scale);
}
/**
@@ -2557,8 +2725,8 @@ public class WebView extends AbsoluteLayout
}
private HitTestResult hitTestResult(HitTestResult fallback) {
- if (mNativeClass == 0) {
- return null;
+ if (mNativeClass == 0 || sDisableNavcache) {
+ return fallback;
}
HitTestResult result = new HitTestResult();
@@ -2570,7 +2738,8 @@ public class WebView extends AbsoluteLayout
if (text != null) {
if (text.startsWith(SCHEME_TEL)) {
result.setType(HitTestResult.PHONE_TYPE);
- result.setExtra(text.substring(SCHEME_TEL.length()));
+ result.setExtra(URLDecoder.decode(text
+ .substring(SCHEME_TEL.length())));
} else if (text.startsWith(SCHEME_MAILTO)) {
result.setType(HitTestResult.EMAIL_TYPE);
result.setExtra(text.substring(SCHEME_MAILTO.length()));
@@ -2640,14 +2809,22 @@ public class WebView extends AbsoluteLayout
}
int contentX = viewToContentX(mLastTouchX + mScrollX);
int contentY = viewToContentY(mLastTouchY + mScrollY);
+ if (mFocusedNode != null && mFocusedNode.mHitTestX == contentX
+ && mFocusedNode.mHitTestY == contentY) {
+ hrefMsg.getData().putString(FocusNodeHref.URL, mFocusedNode.mLinkUrl);
+ hrefMsg.getData().putString(FocusNodeHref.TITLE, mFocusedNode.mAnchorText);
+ hrefMsg.getData().putString(FocusNodeHref.SRC, mFocusedNode.mImageUrl);
+ hrefMsg.sendToTarget();
+ return;
+ }
if (nativeHasCursorNode()) {
Rect cursorBounds = nativeGetCursorRingBounds();
if (!cursorBounds.contains(contentX, contentY)) {
int slop = viewToContentDimension(mNavSlop);
cursorBounds.inset(-slop, -slop);
if (cursorBounds.contains(contentX, contentY)) {
- contentX = (int) cursorBounds.centerX();
- contentY = (int) cursorBounds.centerY();
+ contentX = cursorBounds.centerX();
+ contentY = cursorBounds.centerY();
}
}
}
@@ -3296,6 +3473,7 @@ public class WebView extends AbsoluteLayout
}
cancelSelectDialog();
+ WebCoreThreadWatchdog.pause();
}
}
@@ -3328,6 +3506,15 @@ public class WebView extends AbsoluteLayout
nativeSetPauseDrawing(mNativeClass, false);
}
}
+ // Ensure that the watchdog has a currently valid Context to be able to display
+ // a prompt dialog. For example, if the Activity was finished whilst the WebCore
+ // thread was blocked and the Activity is started again, we may reuse the blocked
+ // thread, but we'll have a new Activity.
+ WebCoreThreadWatchdog.updateContext(mContext);
+ // We get a call to onResume for new WebViews (i.e. mIsPaused will be false). We need
+ // to ensure that the Watchdog thread is running for the new WebView, so call
+ // it outside the if block above.
+ WebCoreThreadWatchdog.resume();
}
/**
@@ -3668,6 +3855,8 @@ public class WebView extends AbsoluteLayout
nativeScrollLayer(mCurrentScrollingLayerId, x, y);
mScrollingLayerRect.left = x;
mScrollingLayerRect.top = y;
+ mWebViewCore.sendMessage(WebViewCore.EventHub.SCROLL_LAYER, mCurrentScrollingLayerId,
+ mScrollingLayerRect);
onScrollChanged(mScrollX, mScrollY, mScrollX, mScrollY);
invalidate();
}
@@ -3818,7 +4007,7 @@ public class WebView extends AbsoluteLayout
if (onDeviceScriptInjectionEnabled) {
ensureAccessibilityScriptInjectorInstance(false);
// neither script injected nor script injection opted out => we inject
- loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT);
+ loadUrl(getScreenReaderInjectingJs());
// TODO: Set this flag after successfull script injection. Maybe upon injection
// the chooser should update the meta tag and we check it to declare success
mAccessibilityScriptInjected = true;
@@ -3832,7 +4021,7 @@ public class WebView extends AbsoluteLayout
} else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED) {
ensureAccessibilityScriptInjectorInstance(false);
// the URL provides accessibility but we still need to add our generic script
- loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT);
+ loadUrl(getScreenReaderInjectingJs());
} else {
Log.e(LOGTAG, "Unknown URL value for the \"axs\" URL parameter: " + axsParameterValue);
}
@@ -3854,6 +4043,17 @@ public class WebView extends AbsoluteLayout
}
/**
+ * Gets JavaScript that injects a screen-reader.
+ *
+ * @return The JavaScript snippet.
+ */
+ private String getScreenReaderInjectingJs() {
+ String screenReaderUrl = Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL);
+ return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl);
+ }
+
+ /**
* Gets the "axs" URL parameter value.
*
* @param url A url to fetch the paramter from.
@@ -4003,7 +4203,7 @@ public class WebView extends AbsoluteLayout
* Gets the WebViewClient
* @return the current WebViewClient instance.
*
- *@hide pending API council approval.
+ * @hide This is an implementation detail.
*/
public WebViewClient getWebViewClient() {
return mCallbackProxy.getWebViewClient();
@@ -4035,7 +4235,7 @@ public class WebView extends AbsoluteLayout
* Gets the chrome handler.
* @return the current WebChromeClient instance.
*
- * @hide API council approval.
+ * @hide This is an implementation detail.
*/
public WebChromeClient getWebChromeClient() {
return mCallbackProxy.getWebChromeClient();
@@ -4089,34 +4289,39 @@ public class WebView extends AbsoluteLayout
}
/**
- * Use this function to bind an object to JavaScript so that the
- * methods can be accessed from JavaScript.
+ * This method injects the supplied Java object into the WebView. The
+ * object is injected into the JavaScript context of the main frame, using
+ * the supplied name. This allows the Java object to be accessed from
+ * JavaScript. Note that that injected objects will not appear in
+ * JavaScript until the page is next (re)loaded. For example:
+ * <pre> webView.addJavascriptInterface(new Object(), "injectedObject");
+ * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null);
+ * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre>
* <p><strong>IMPORTANT:</strong>
* <ul>
- * <li> Using addJavascriptInterface() allows JavaScript to control your
- * application. This can be a very useful feature or a dangerous security
- * issue. When the HTML in the WebView is untrustworthy (for example, part
- * or all of the HTML is provided by some person or process), then an
- * attacker could inject HTML that will execute your code and possibly any
- * code of the attacker's choosing.<br>
- * Do not use addJavascriptInterface() unless all of the HTML in this
- * WebView was written by you.</li>
- * <li> The Java object that is bound runs in another thread and not in
- * the thread that it was constructed in.</li>
+ * <li> addJavascriptInterface() can be used to allow JavaScript to control
+ * the host application. This is a powerful feature, but also presents a
+ * security risk. Use of this method in a WebView containing untrusted
+ * content could allow an attacker to manipulate the host application in
+ * unintended ways, executing Java code with the permissions of the host
+ * application. Use extreme care when using this method in a WebView which
+ * could contain untrusted content.
+ * <li> JavaScript interacts with Java object on a private, background
+ * thread of the WebView. Care is therefore required to maintain thread
+ * safety.</li>
* </ul></p>
- * @param obj The class instance to bind to JavaScript, null instances are
- * ignored.
- * @param interfaceName The name to used to expose the instance in
- * JavaScript.
+ * @param object The Java object to inject into the WebView's JavaScript
+ * context. Null values are ignored.
+ * @param name The name used to expose the instance in JavaScript.
*/
- public void addJavascriptInterface(Object obj, String interfaceName) {
+ public void addJavascriptInterface(Object object, String name) {
checkThread();
- if (obj == null) {
+ if (object == null) {
return;
}
WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData();
- arg.mObject = obj;
- arg.mInterfaceName = interfaceName;
+ arg.mObject = object;
+ arg.mInterfaceName = name;
mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg);
}
@@ -4245,6 +4450,9 @@ public class WebView extends AbsoluteLayout
@Override
protected void onDraw(Canvas canvas) {
+ if (inFullScreenMode()) {
+ return; // no need to draw anything if we aren't visible.
+ }
// if mNativeClass is 0, the WebView is either destroyed or not
// initialized. In either case, just draw the background color and return
if (mNativeClass == 0) {
@@ -4261,13 +4469,6 @@ public class WebView extends AbsoluteLayout
}
if (canvas.isHardwareAccelerated()) {
- if (mIncrementEGLContextHack == false) {
- mIncrementEGLContextHack = true;
- EGL10 egl = (EGL10) EGLContext.getEGL();
- EGLDisplay eglDisplay = egl.eglGetDisplay(EGL_DEFAULT_DISPLAY);
- int[] version = new int[2];
- egl.eglInitialize(eglDisplay, version);
- }
mZoomManager.setHardwareAccelerated();
}
@@ -4285,7 +4486,7 @@ public class WebView extends AbsoluteLayout
|| mTouchMode == TOUCH_SHORTPRESS_MODE
|| mTouchMode == TOUCH_DONE_MODE);
boolean drawNativeRings = !drawJavaRings;
- if (USE_WEBKIT_RINGS) {
+ if (sDisableNavcache) {
drawNativeRings = !drawJavaRings && !isInTouchMode();
}
drawContent(canvas, drawNativeRings);
@@ -4307,10 +4508,6 @@ public class WebView extends AbsoluteLayout
Rect r = mTouchHighlightRegion.getBounds();
postInvalidateDelayed(delay, r.left, r.top, r.right, r.bottom);
} else {
- if (mTouchHightlightPaint == null) {
- mTouchHightlightPaint = new Paint();
- mTouchHightlightPaint.setColor(HIGHLIGHT_COLOR);
- }
RegionIterator iter = new RegionIterator(mTouchHighlightRegion);
Rect r = new Rect();
while (iter.next(r)) {
@@ -4340,8 +4537,8 @@ public class WebView extends AbsoluteLayout
}
private void removeTouchHighlight() {
- mWebViewCore.removeMessages(EventHub.GET_TOUCH_HIGHLIGHT_RECTS);
- mPrivateHandler.removeMessages(SET_TOUCH_HIGHLIGHT_RECTS);
+ mWebViewCore.removeMessages(EventHub.HIT_TEST);
+ mPrivateHandler.removeMessages(HIT_TEST_RESULT);
setTouchHighlightRects(null);
}
@@ -4403,6 +4600,11 @@ public class WebView extends AbsoluteLayout
final boolean isSelecting = selectText();
if (isSelecting) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ } else if (focusCandidateIsEditableText()) {
+ mSelectCallback = new SelectActionModeCallback();
+ mSelectCallback.setWebView(this);
+ mSelectCallback.setTextSelected(false);
+ startActionMode(mSelectCallback);
}
return isSelecting;
}
@@ -4410,7 +4612,7 @@ public class WebView extends AbsoluteLayout
/**
* Select the word at the last click point.
*
- * @hide pending API council approval
+ * @hide This is an implementation detail.
*/
public boolean selectText() {
int x = viewToContentX(mLastTouchX + mScrollX);
@@ -4518,7 +4720,7 @@ public class WebView extends AbsoluteLayout
boolean isPictureAfterFirstLayout, boolean registerPageSwapCallback) {
if (mNativeClass == 0)
return;
- nativeSetBaseLayer(layer, invalRegion, showVisualIndicator,
+ nativeSetBaseLayer(mNativeClass, layer, invalRegion, showVisualIndicator,
isPictureAfterFirstLayout, registerPageSwapCallback);
if (mHTML5VideoViewProxy != null) {
mHTML5VideoViewProxy.setBaseLayer(layer);
@@ -4826,9 +5028,19 @@ public class WebView extends AbsoluteLayout
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- InputConnection connection = super.onCreateInputConnection(outAttrs);
- outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
- return connection;
+ outAttrs.inputType = EditorInfo.IME_FLAG_NO_FULLSCREEN
+ | EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
+ | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
+
+ if (mInputConnection == null) {
+ mInputConnection = new WebViewInputConnection();
+ }
+ outAttrs.initialCapsMode = mInputConnection.getCursorCapsMode(InputType.TYPE_CLASS_TEXT);
+ return mInputConnection;
}
/**
@@ -5023,6 +5235,7 @@ public class WebView extends AbsoluteLayout
mWebSettings = getSettings();
}
+ @Override
public void run() {
ArrayList<String> pastEntries = new ArrayList<String>();
@@ -5107,17 +5320,6 @@ public class WebView extends AbsoluteLayout
canProvideGamma, gamma);
}
- /**
- * Dump the V8 counters to standard output.
- * Note that you need a build with V8 and WEBCORE_INSTRUMENTATION set to
- * true. Otherwise, this will do nothing.
- *
- * @hide debug only
- */
- public void dumpV8Counters() {
- mWebViewCore.sendMessage(EventHub.DUMP_V8COUNTERS);
- }
-
// This is used to determine long press with the center key. Does not
// affect long press with the trackball/touch.
private boolean mGotCenterDown = false;
@@ -5307,9 +5509,6 @@ public class WebView extends AbsoluteLayout
case KeyEvent.KEYCODE_8:
dumpRenderTree(keyCode == KeyEvent.KEYCODE_7);
break;
- case KeyEvent.KEYCODE_9:
- nativeInstrumentReport();
- return true;
}
}
@@ -5562,7 +5761,7 @@ public class WebView extends AbsoluteLayout
/**
* Select all of the text in this WebView.
*
- * @hide pending API council approval.
+ * @hide This is an implementation detail.
*/
public void selectAll() {
if (0 == mNativeClass) return; // client isn't initialized
@@ -5605,7 +5804,7 @@ public class WebView extends AbsoluteLayout
/**
* Copy the selection to the clipboard
*
- * @hide pending API council approval.
+ * @hide This is an implementation detail.
*/
public boolean copySelection() {
boolean copiedSomething = false;
@@ -5621,13 +5820,50 @@ public class WebView extends AbsoluteLayout
ClipboardManager cm = (ClipboardManager)getContext()
.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setText(selection);
+ int[] handles = new int[4];
+ nativeGetSelectionHandles(mNativeClass, handles);
+ mWebViewCore.sendMessage(EventHub.COPY_TEXT, handles);
}
invalidate(); // remove selection region and pointer
return copiedSomething;
}
/**
- * @hide pending API Council approval.
+ * Cut the selected text into the clipboard
+ *
+ * @hide This is an implementation detail
+ */
+ public void cutSelection() {
+ copySelection();
+ int[] handles = new int[4];
+ nativeGetSelectionHandles(mNativeClass, handles);
+ mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles);
+ }
+
+ /**
+ * Paste text from the clipboard to the cursor position.
+ *
+ * @hide This is an implementation detail
+ */
+ public void pasteFromClipboard() {
+ ClipboardManager cm = (ClipboardManager)getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = cm.getPrimaryClip();
+ if (clipData != null) {
+ ClipData.Item clipItem = clipData.getItemAt(0);
+ CharSequence pasteText = clipItem.getText();
+ if (pasteText != null) {
+ int[] handles = new int[4];
+ nativeGetSelectionHandles(mNativeClass, handles);
+ mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles);
+ mWebViewCore.sendMessage(EventHub.INSERT_TEXT,
+ pasteText.toString());
+ }
+ }
+ }
+
+ /**
+ * @hide This is an implementation detail.
*/
public SearchBox getSearchBox() {
if ((mWebViewCore == null) || (mWebViewCore.getBrowserFrame() == null)) {
@@ -5699,6 +5935,8 @@ public class WebView extends AbsoluteLayout
* @deprecated WebView no longer needs to implement
* ViewGroup.OnHierarchyChangeListener. This method does nothing now.
*/
+ @Override
+ // Cannot add @hide as this can always be accessed via the interface.
@Deprecated
public void onChildViewAdded(View parent, View child) {}
@@ -5706,6 +5944,8 @@ public class WebView extends AbsoluteLayout
* @deprecated WebView no longer needs to implement
* ViewGroup.OnHierarchyChangeListener. This method does nothing now.
*/
+ @Override
+ // Cannot add @hide as this can always be accessed via the interface.
@Deprecated
public void onChildViewRemoved(View p, View child) {}
@@ -5713,6 +5953,8 @@ public class WebView extends AbsoluteLayout
* @deprecated WebView should not have implemented
* ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now.
*/
+ @Override
+ // Cannot add @hide as this can always be accessed via the interface.
@Deprecated
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
}
@@ -5842,7 +6084,8 @@ public class WebView extends AbsoluteLayout
}
calcOurContentVisibleRectF(mVisibleContentRect);
nativeUpdateDrawGLFunction(mGLViewportEmpty ? null : mGLRectViewport,
- mGLViewportEmpty ? null : mViewRectViewport, mVisibleContentRect);
+ mGLViewportEmpty ? null : mViewRectViewport,
+ mVisibleContentRect);
}
/**
@@ -5989,6 +6232,7 @@ public class WebView extends AbsoluteLayout
if (inFullScreenMode()) {
mFullScreenHolder.hide();
mFullScreenHolder = null;
+ invalidate();
}
}
@@ -6106,7 +6350,7 @@ public class WebView extends AbsoluteLayout
nativeSetIsScrolling(false);
} else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) {
mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP);
- if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) {
+ if (sDisableNavcache) {
removeTouchHighlight();
}
if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) {
@@ -6130,7 +6374,7 @@ public class WebView extends AbsoluteLayout
mWebViewCore.sendMessage(
EventHub.UPDATE_FRAME_CACHE_IF_LOADING);
}
- if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) {
+ if (sDisableNavcache) {
TouchHighlightData data = new TouchHighlightData();
data.mX = contentX;
data.mY = contentY;
@@ -6142,13 +6386,14 @@ public class WebView extends AbsoluteLayout
if (!mBlockWebkitViewMessages) {
mTouchHighlightRequested = System.currentTimeMillis();
mWebViewCore.sendMessageAtFrontOfQueue(
- EventHub.GET_TOUCH_HIGHLIGHT_RECTS, data);
+ EventHub.HIT_TEST, data);
}
if (DEBUG_TOUCH_HIGHLIGHT) {
if (getSettings().getNavDump()) {
- mTouchHighlightX = (int) x + mScrollX;
- mTouchHighlightY = (int) y + mScrollY;
+ mTouchHighlightX = x + mScrollX;
+ mTouchHighlightY = y + mScrollY;
mPrivateHandler.postDelayed(new Runnable() {
+ @Override
public void run() {
mTouchHighlightX = mTouchHighlightY = 0;
invalidate();
@@ -6229,7 +6474,7 @@ public class WebView extends AbsoluteLayout
if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) {
mTouchMode = TOUCH_INIT_MODE;
}
- if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) {
+ if (sDisableNavcache) {
removeTouchHighlight();
}
}
@@ -6740,8 +6985,6 @@ public class WebView extends AbsoluteLayout
int oldY = mScrollY;
int rangeX = computeMaxScrollX();
int rangeY = computeMaxScrollY();
- int overscrollDistance = mOverscrollDistance;
-
// Check for the original scrolling layer in case we change
// directions. mTouchMode might be TOUCH_DRAG_MODE if we have
// reached the edge of a layer but mScrollingLayer will be non-zero
@@ -6833,7 +7076,7 @@ public class WebView extends AbsoluteLayout
mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS);
mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS);
- if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) {
+ if (sDisableNavcache) {
removeTouchHighlight();
}
mHeldMotionless = MOTIONLESS_TRUE;
@@ -7390,7 +7633,7 @@ public class WebView extends AbsoluteLayout
* and calls showCursorTimed on the native side
*/
private void updateSelection() {
- if (mNativeClass == 0) {
+ if (mNativeClass == 0 || sDisableNavcache) {
return;
}
mPrivateHandler.removeMessages(UPDATE_SELECTION);
@@ -7454,8 +7697,8 @@ public class WebView extends AbsoluteLayout
return false;
}
mDragFromTextInput = true;
- event.offsetLocation((float) (mWebTextView.getLeft() - mScrollX),
- (float) (mWebTextView.getTop() - mScrollY));
+ event.offsetLocation((mWebTextView.getLeft() - mScrollX),
+ (mWebTextView.getTop() - mScrollY));
boolean result = onTouchEvent(event);
mDragFromTextInput = false;
return result;
@@ -7498,7 +7741,7 @@ public class WebView extends AbsoluteLayout
int contentX = viewToContentX(mLastTouchX + mScrollX);
int contentY = viewToContentY(mLastTouchY + mScrollY);
int slop = viewToContentDimension(mNavSlop);
- if (USE_WEBKIT_RINGS && !mTouchHighlightRegion.isEmpty()) {
+ if (sDisableNavcache && !mTouchHighlightRegion.isEmpty()) {
// set mTouchHighlightRequested to 0 to cause an immediate
// drawing of the touch rings
mTouchHighlightRequested = 0;
@@ -7510,8 +7753,7 @@ public class WebView extends AbsoluteLayout
}
}, ViewConfiguration.getPressedStateDuration());
}
- if (getSettings().supportTouchOnly()) {
- removeTouchHighlight();
+ if (sDisableNavcache) {
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
@@ -8396,9 +8638,8 @@ public class WebView extends AbsoluteLayout
break;
}
case SWITCH_TO_SHORTPRESS: {
- mInitialHitTestResult = null; // set by updateSelection()
if (mTouchMode == TOUCH_INIT_MODE) {
- if (!getSettings().supportTouchOnly()
+ if (!sDisableNavcache
&& mPreventDefault != PREVENT_DEFAULT_YES) {
mTouchMode = TOUCH_SHORTPRESS_START_MODE;
updateSelection();
@@ -8413,7 +8654,7 @@ public class WebView extends AbsoluteLayout
break;
}
case SWITCH_TO_LONGPRESS: {
- if (USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) {
+ if (sDisableNavcache) {
removeTouchHighlight();
}
if (inFullScreenMode() || mDeferTouchProcess) {
@@ -8652,6 +8893,12 @@ public class WebView extends AbsoluteLayout
}
break;
+ case EXIT_FULLSCREEN_VIDEO:
+ if (mHTML5VideoViewProxy != null) {
+ mHTML5VideoViewProxy.exitFullScreenVideo();
+ }
+ break;
+
case SHOW_FULLSCREEN: {
View view = (View) msg.obj;
int orientation = msg.arg1;
@@ -8664,6 +8911,7 @@ public class WebView extends AbsoluteLayout
mFullScreenHolder = new PluginFullScreenHolder(WebView.this, orientation, npp);
mFullScreenHolder.setContentView(view);
mFullScreenHolder.show();
+ invalidate();
break;
}
@@ -8743,10 +8991,28 @@ public class WebView extends AbsoluteLayout
}
break;
- case SET_TOUCH_HIGHLIGHT_RECTS:
- @SuppressWarnings("unchecked")
- ArrayList<Rect> rects = (ArrayList<Rect>) msg.obj;
- setTouchHighlightRects(rects);
+ case HIT_TEST_RESULT:
+ WebKitHitTest hit = (WebKitHitTest) msg.obj;
+ mFocusedNode = hit;
+ setTouchHighlightRects(hit);
+ if (hit == null) {
+ mInitialHitTestResult = null;
+ } else {
+ mInitialHitTestResult = new HitTestResult();
+ if (hit.mLinkUrl != null) {
+ mInitialHitTestResult.mType = HitTestResult.SRC_ANCHOR_TYPE;
+ mInitialHitTestResult.mExtra = hit.mLinkUrl;
+ if (hit.mImageUrl != null) {
+ mInitialHitTestResult.mType = HitTestResult.SRC_IMAGE_ANCHOR_TYPE;
+ mInitialHitTestResult.mExtra = hit.mImageUrl;
+ }
+ } else if (hit.mImageUrl != null) {
+ mInitialHitTestResult.mType = HitTestResult.IMAGE_TYPE;
+ mInitialHitTestResult.mExtra = hit.mImageUrl;
+ } else if (hit.mEditable) {
+ mInitialHitTestResult.mType = HitTestResult.EDIT_TEXT_TYPE;
+ }
+ }
break;
case SAVE_WEBARCHIVE_FINISHED:
@@ -8776,6 +9042,35 @@ public class WebView extends AbsoluteLayout
nativeSelectAt(msg.arg1, msg.arg2);
break;
+ case COPY_TO_CLIPBOARD:
+ copyToClipboard((String) msg.obj);
+ break;
+
+ case INIT_EDIT_FIELD:
+ if (mInputConnection != null) {
+ mTextGeneration = 0;
+ String text = (String)msg.obj;
+ mInputConnection.beginBatchEdit();
+ Editable editable = mInputConnection.getEditable();
+ editable.replace(0, editable.length(), text);
+ int start = msg.arg1;
+ int end = msg.arg2;
+ mInputConnection.setComposingRegion(end, end);
+ mInputConnection.setSelection(start, end);
+ mInputConnection.endBatchEdit();
+ }
+ break;
+
+ case REPLACE_TEXT:{
+ String text = (String)msg.obj;
+ int start = msg.arg1;
+ int end = msg.arg2;
+ int cursorPosition = start + text.length();
+ replaceTextfieldText(start, end, text,
+ cursorPosition, cursorPosition);
+ break;
+ }
+
default:
super.handleMessage(msg);
break;
@@ -8783,10 +9078,14 @@ public class WebView extends AbsoluteLayout
}
}
- private void setTouchHighlightRects(ArrayList<Rect> rects) {
- invalidate(mTouchHighlightRegion.getBounds());
- mTouchHighlightRegion.setEmpty();
+ private void setTouchHighlightRects(WebKitHitTest hit) {
+ Rect[] rects = hit != null ? hit.mTouchRects : null;
+ if (!mTouchHighlightRegion.isEmpty()) {
+ invalidate(mTouchHighlightRegion.getBounds());
+ mTouchHighlightRegion.setEmpty();
+ }
if (rects != null) {
+ mTouchHightlightPaint.setColor(hit.mTapHighlightColor);
for (Rect rect : rects) {
Rect viewRect = contentToViewRect(rect);
// some sites, like stories in nytimes.com, set
@@ -8897,10 +9196,13 @@ public class WebView extends AbsoluteLayout
*/
private void updateTextSelectionFromMessage(int nodePointer,
int textGeneration, WebViewCore.TextSelectionData data) {
- if (inEditingMode()
- && mWebTextView.isSameTextField(nodePointer)
- && textGeneration == mTextGeneration) {
- mWebTextView.setSelectionFromWebKit(data.mStart, data.mEnd);
+ if (textGeneration == mTextGeneration) {
+ if (inEditingMode()
+ && mWebTextView.isSameTextField(nodePointer)) {
+ mWebTextView.setSelectionFromWebKit(data.mStart, data.mEnd);
+ } else if (mInputConnection != null){
+ mInputConnection.setSelection(data.mStart, data.mEnd);
+ }
}
}
@@ -9009,7 +9311,7 @@ public class WebView extends AbsoluteLayout
if (position < 0 || position >= getCount()) {
return null;
}
- return (Container) getItem(position);
+ return getItem(position);
}
@Override
@@ -9108,6 +9410,7 @@ public class WebView extends AbsoluteLayout
}
}
+ @Override
public void run() {
final ListView listView = (ListView) LayoutInflater.from(mContext)
.inflate(com.android.internal.R.layout.select_dialog, null);
@@ -9118,6 +9421,7 @@ public class WebView extends AbsoluteLayout
if (mMultiple) {
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int which) {
mWebViewCore.sendMessage(
EventHub.LISTBOX_CHOICES,
@@ -9126,6 +9430,7 @@ public class WebView extends AbsoluteLayout
}});
b.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int which) {
mWebViewCore.sendMessage(
EventHub.SINGLE_LISTBOX_CHOICE, -2, 0);
@@ -9149,6 +9454,7 @@ public class WebView extends AbsoluteLayout
}
} else {
listView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
public void onItemClick(AdapterView<?> parent, View v,
int position, long id) {
// Rather than sending the message right away, send it
@@ -9169,6 +9475,7 @@ public class WebView extends AbsoluteLayout
}
}
mListBoxDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
public void onCancel(DialogInterface dialog) {
mWebViewCore.sendMessage(
EventHub.SINGLE_LISTBOX_CHOICE, -2, 0);
@@ -9445,6 +9752,18 @@ public class WebView extends AbsoluteLayout
}
/**
+ * Copy text into the clipboard. This is called indirectly from
+ * WebViewCore.
+ * @param text The text to put into the clipboard.
+ */
+ private void copyToClipboard(String text) {
+ ClipboardManager cm = (ClipboardManager)getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText(getTitle(), text);
+ cm.setPrimaryClip(clip);
+ }
+
+ /**
* Update our cache with updatedText.
* @param updatedText The new text to put in our cache.
* @hide
@@ -9484,7 +9803,12 @@ public class WebView extends AbsoluteLayout
/** @hide call pageSwapCallback upon next page swap */
protected void registerPageSwapCallback() {
- nativeRegisterPageSwapCallback();
+ nativeRegisterPageSwapCallback(mNativeClass);
+ }
+
+ /** @hide discard all textures from tiles */
+ protected void discardAllTextures() {
+ nativeDiscardAllTextures();
}
/**
@@ -9525,6 +9849,23 @@ public class WebView extends AbsoluteLayout
return nativeTileProfilingGetFloat(frame, tile, key);
}
+ /**
+ * Checks the focused content for an editable text field. This can be
+ * text input or ContentEditable.
+ * @return true if the focused item is an editable text field.
+ */
+ boolean focusCandidateIsEditableText() {
+ boolean isEditable = false;
+ // TODO: reverse sDisableNavcache so that its name is positive
+ boolean isNavcacheEnabled = !sDisableNavcache;
+ if (isNavcacheEnabled) {
+ isEditable = nativeFocusCandidateIsEditableText(mNativeClass);
+ } else if (mFocusedNode != null) {
+ isEditable = mFocusedNode.mEditable;
+ }
+ return isEditable;
+ }
+
private native int nativeCacheHitFramePointer();
private native boolean nativeCacheHitIsPlugin();
private native Rect nativeCacheHitNodeBounds();
@@ -9570,6 +9911,7 @@ public class WebView extends AbsoluteLayout
/* package */ native boolean nativeFocusCandidateIsPassword();
private native boolean nativeFocusCandidateIsRtlText();
private native boolean nativeFocusCandidateIsTextInput();
+ private native boolean nativeFocusCandidateIsEditableText(int nativeClass);
/* package */ native int nativeFocusCandidateMaxLength();
/* package */ native boolean nativeFocusCandidateIsAutoComplete();
/* package */ native boolean nativeFocusCandidateIsSpellcheck();
@@ -9602,7 +9944,6 @@ public class WebView extends AbsoluteLayout
private native void nativeHideCursor();
private native boolean nativeHitSelection(int x, int y);
private native String nativeImageURI(int x, int y);
- private native void nativeInstrumentReport();
private native Rect nativeLayerBounds(int layer);
/* package */ native boolean nativeMoveCursorToNextTextInput();
// return true if the page has been scrolled
@@ -9635,7 +9976,8 @@ public class WebView extends AbsoluteLayout
private native void nativeSetFindIsEmpty();
private native void nativeSetFindIsUp(boolean isUp);
private native void nativeSetHeightCanMeasure(boolean measure);
- private native void nativeSetBaseLayer(int layer, Region invalRegion,
+ private native void nativeSetBaseLayer(int nativeInstance,
+ int layer, Region invalRegion,
boolean showVisualIndicator, boolean isPictureAfterFirstLayout,
boolean registerPageSwapCallback);
private native int nativeGetBaseLayer();
@@ -9649,7 +9991,8 @@ public class WebView extends AbsoluteLayout
private native void nativeStopGL();
private native Rect nativeSubtractLayers(Rect content);
private native int nativeTextGeneration();
- private native void nativeRegisterPageSwapCallback();
+ private native void nativeRegisterPageSwapCallback(int nativeInstance);
+ private native void nativeDiscardAllTextures();
private native void nativeTileProfilingStart();
private native float nativeTileProfilingStop();
private native void nativeTileProfilingClear();
@@ -9690,4 +10033,5 @@ public class WebView extends AbsoluteLayout
*/
private static native void nativeOnTrimMemory(int level);
private static native void nativeSetPauseDrawing(int instance, boolean pause);
+ private static native boolean nativeDisableNavcache();
}
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
index 2d15afb..fe51581 100644
--- a/core/java/android/webkit/WebViewCore.java
+++ b/core/java/android/webkit/WebViewCore.java
@@ -26,6 +26,7 @@ import android.graphics.Region;
import android.media.MediaFile;
import android.net.ProxyProperties;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -37,18 +38,15 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
-import android.webkit.DeviceMotionService;
-import android.webkit.DeviceMotionAndOrientationManager;
-import android.webkit.DeviceOrientationService;
-import android.webkit.JniUtil;
+import android.webkit.WebView.FocusNodeHref;
+
+import junit.framework.Assert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
-import junit.framework.Assert;
-
/**
* @hide
*/
@@ -170,6 +168,10 @@ public final class WebViewCore {
"creation.");
Log.e(LOGTAG, Log.getStackTraceString(e));
}
+
+ // Start the singleton watchdog which will monitor the WebCore thread
+ // to verify it's still processing messages.
+ WebCoreThreadWatchdog.start(context, sWebCoreHandler);
}
}
// Create an EventHub to handle messages before and after the thread is
@@ -382,8 +384,9 @@ public final class WebViewCore {
mCallbackProxy.onExceededDatabaseQuota(url, databaseIdentifier,
currentQuota, estimatedSize, getUsedQuota(),
new WebStorage.QuotaUpdater() {
+ @Override
public void updateQuota(long quota) {
- nativeSetNewStorageLimit(quota);
+ nativeSetNewStorageLimit(mNativeClass, quota);
}
});
}
@@ -396,14 +399,16 @@ public final class WebViewCore {
protected void reachedMaxAppCacheSize(long spaceNeeded) {
mCallbackProxy.onReachedMaxAppCacheSize(spaceNeeded, getUsedQuota(),
new WebStorage.QuotaUpdater() {
+ @Override
public void updateQuota(long quota) {
- nativeSetNewStorageLimit(quota);
+ nativeSetNewStorageLimit(mNativeClass, quota);
}
});
}
protected void populateVisitedLinks() {
ValueCallback callback = new ValueCallback<String[]>() {
+ @Override
public void onReceiveValue(String[] value) {
sendMessage(EventHub.POPULATE_VISITED_LINKS, (Object)value);
}
@@ -420,14 +425,15 @@ public final class WebViewCore {
protected void geolocationPermissionsShowPrompt(String origin) {
mCallbackProxy.onGeolocationPermissionsShowPrompt(origin,
new GeolocationPermissions.Callback() {
- public void invoke(String origin, boolean allow, boolean remember) {
- GeolocationPermissionsData data = new GeolocationPermissionsData();
- data.mOrigin = origin;
- data.mAllow = allow;
- data.mRemember = remember;
- // Marshall to WebCore thread.
- sendMessage(EventHub.GEOLOCATION_PERMISSIONS_PROVIDE, data);
- }
+ @Override
+ public void invoke(String origin, boolean allow, boolean remember) {
+ GeolocationPermissionsData data = new GeolocationPermissionsData();
+ data.mOrigin = origin;
+ data.mAllow = allow;
+ data.mRemember = remember;
+ // Marshall to WebCore thread.
+ sendMessage(EventHub.GEOLOCATION_PERMISSIONS_PROVIDE, data);
+ }
});
}
@@ -497,6 +503,24 @@ public final class WebViewCore {
message.sendToTarget();
}
+ /**
+ * Notify the webview that we want to exit the video fullscreen.
+ * This is called through JNI by webcore.
+ */
+ protected void exitFullscreenVideo() {
+ if (mWebView == null) return;
+ Message message = Message.obtain(mWebView.mPrivateHandler,
+ WebView.EXIT_FULLSCREEN_VIDEO);
+ message.sendToTarget();
+ }
+
+ /**
+ * Clear the picture set. To be called only on the WebCore thread.
+ */
+ /* package */ void clearContent() {
+ nativeClearContent(mNativeClass);
+ }
+
//-------------------------------------------------------------------------
// JNI methods
//-------------------------------------------------------------------------
@@ -506,15 +530,16 @@ public final class WebViewCore {
/**
* Empty the picture set.
*/
- private native void nativeClearContent();
+ private native void nativeClearContent(int nativeClass);
- private native void nativeContentInvalidateAll();
+ private native void nativeContentInvalidateAll(int nativeClass);
/**
* Redraw a portion of the picture set. The Point wh returns the
* width and height of the overall picture.
*/
- private native int nativeRecordContent(Region invalRegion, Point wh);
+ private native int nativeRecordContent(int nativeClass, Region invalRegion,
+ Point wh);
/**
* Update the layers' content
@@ -526,25 +551,27 @@ public final class WebViewCore {
*/
private native void nativeNotifyAnimationStarted(int nativeClass);
- private native boolean nativeFocusBoundsChanged();
+ private native boolean nativeFocusBoundsChanged(int nativeClass);
/**
* Splits slow parts of the picture set. Called from the webkit thread after
* WebView.nativeDraw() returns content to be split.
*/
- private native void nativeSplitContent(int content);
+ private native void nativeSplitContent(int nativeClass, int content);
- private native boolean nativeKey(int keyCode, int unichar,
- int repeatCount, boolean isShift, boolean isAlt, boolean isSym,
- boolean isDown);
+ private native boolean nativeKey(int nativeClass, int keyCode,
+ int unichar, int repeatCount, boolean isShift, boolean isAlt,
+ boolean isSym, boolean isDown);
- private native void nativeClick(int framePtr, int nodePtr, boolean fake);
+ private native void nativeClick(int nativeClass, int framePtr, int nodePtr,
+ boolean fake);
- private native void nativeSendListBoxChoices(boolean[] choices, int size);
+ private native void nativeSendListBoxChoices(int nativeClass,
+ boolean[] choices, int size);
- private native void nativeSendListBoxChoice(int choice);
+ private native void nativeSendListBoxChoice(int nativeClass, int choice);
- private native void nativeCloseIdleConnections();
+ private native void nativeCloseIdleConnections(int nativeClass);
/* Tell webkit what its width and height are, for the purposes
of layout/line-breaking. These coordinates are in document space,
@@ -554,77 +581,84 @@ public final class WebViewCore {
fixed size, textWrapWidth can be different from width with zooming.
should this be called nativeSetViewPortSize?
*/
- private native void nativeSetSize(int width, int height, int textWrapWidth,
- float scale, int screenWidth, int screenHeight, int anchorX,
- int anchorY, boolean ignoreHeight);
+ private native void nativeSetSize(int nativeClass, int width, int height,
+ int textWrapWidth, float scale, int screenWidth, int screenHeight,
+ int anchorX, int anchorY, boolean ignoreHeight);
- private native int nativeGetContentMinPrefWidth();
+ private native int nativeGetContentMinPrefWidth(int nativeClass);
// Start: functions that deal with text editing
private native void nativeReplaceTextfieldText(
- int oldStart, int oldEnd, String replace, int newStart, int newEnd,
- int textGeneration);
+ int nativeClass, int oldStart, int oldEnd, String replace,
+ int newStart, int newEnd, int textGeneration);
- private native void passToJs(int gen,
- String currentText, int keyCode, int keyValue, boolean down,
- boolean cap, boolean fn, boolean sym);
+ private native void passToJs(int nativeClass,
+ int gen, String currentText, int keyCode, int keyValue,
+ boolean down, boolean cap, boolean fn, boolean sym);
- private native void nativeSetFocusControllerActive(boolean active);
+ private native void nativeSetFocusControllerActive(int nativeClass,
+ boolean active);
- private native void nativeSaveDocumentState(int frame);
+ private native void nativeSaveDocumentState(int nativeClass, int frame);
- private native void nativeMoveFocus(int framePtr, int nodePointer);
- private native void nativeMoveMouse(int framePtr, int x, int y);
+ private native void nativeMoveFocus(int nativeClass, int framePtr,
+ int nodePointer);
+ private native void nativeMoveMouse(int nativeClass, int framePtr, int x,
+ int y);
- private native void nativeMoveMouseIfLatest(int moveGeneration,
- int framePtr, int x, int y);
+ private native void nativeMoveMouseIfLatest(int nativeClass,
+ int moveGeneration, int framePtr, int x, int y);
- private native String nativeRetrieveHref(int x, int y);
- private native String nativeRetrieveAnchorText(int x, int y);
- private native String nativeRetrieveImageSource(int x, int y);
- private native void nativeStopPaintingCaret();
- private native void nativeTouchUp(int touchGeneration,
- int framePtr, int nodePtr, int x, int y);
+ private native String nativeRetrieveHref(int nativeClass, int x, int y);
+ private native String nativeRetrieveAnchorText(int nativeClass,
+ int x, int y);
+ private native String nativeRetrieveImageSource(int nativeClass,
+ int x, int y);
+ private native void nativeStopPaintingCaret(int nativeClass);
+ private native void nativeTouchUp(int nativeClass,
+ int touchGeneration, int framePtr, int nodePtr, int x, int y);
- private native boolean nativeHandleTouchEvent(int action, int[] idArray,
- int[] xArray, int[] yArray, int count, int actionIndex, int metaState);
+ private native boolean nativeHandleTouchEvent(int nativeClass, int action,
+ int[] idArray, int[] xArray, int[] yArray, int count,
+ int actionIndex, int metaState);
- private native void nativeUpdateFrameCache();
+ private native void nativeUpdateFrameCache(int nativeClass);
- private native void nativeSetBackgroundColor(int color);
+ private native void nativeSetBackgroundColor(int nativeClass, int color);
- private native void nativeDumpDomTree(boolean useFile);
+ private native void nativeDumpDomTree(int nativeClass, boolean useFile);
- private native void nativeDumpRenderTree(boolean useFile);
+ private native void nativeDumpRenderTree(int nativeClass, boolean useFile);
- private native void nativeDumpNavTree();
+ private native void nativeDumpNavTree(int nativeClass);
- private native void nativeDumpV8Counters();
-
- private native void nativeSetJsFlags(String flags);
+ private native void nativeSetJsFlags(int nativeClass, String flags);
/**
* Delete text from start to end in the focused textfield. If there is no
* focus, or if start == end, silently fail. If start and end are out of
* order, swap them.
- * @param start Beginning of selection to delete.
- * @param end End of selection to delete.
- * @param textGeneration Text generation number when delete was pressed.
+ * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass
+ * @param start Beginning of selection to delete.
+ * @param end End of selection to delete.
+ * @param textGeneration Text generation number when delete was pressed.
*/
- private native void nativeDeleteSelection(int start, int end,
- int textGeneration);
+ private native void nativeDeleteSelection(int nativeClass, int start,
+ int end, int textGeneration);
/**
* Set the selection to (start, end) in the focused textfield. If start and
* end are out of order, swap them.
- * @param start Beginning of selection.
- * @param end End of selection.
+ * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass
+ * @param start Beginning of selection.
+ * @param end End of selection.
*/
- private native void nativeSetSelection(int start, int end);
+ private native void nativeSetSelection(int nativeClass, int start, int end);
// Register a scheme to be treated as local scheme so that it can access
// local asset files for resources
- private native void nativeRegisterURLSchemeAsLocal(String scheme);
+ private native void nativeRegisterURLSchemeAsLocal(int nativeClass,
+ String scheme);
/*
* Inform webcore that the user has decided whether to allow or deny new
@@ -632,34 +666,39 @@ public final class WebViewCore {
* the main thread should wake up now.
* @param limit Is the new quota for an origin or new app cache max size.
*/
- private native void nativeSetNewStorageLimit(long limit);
+ private native void nativeSetNewStorageLimit(int nativeClass, long limit);
/**
* Provide WebCore with a Geolocation permission state for the specified
* origin.
+ * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass
* @param origin The origin for which Geolocation permissions are provided.
* @param allow Whether Geolocation permissions are allowed.
* @param remember Whether this decision should be remembered beyond the
* life of the current page.
*/
- private native void nativeGeolocationPermissionsProvide(String origin, boolean allow, boolean remember);
+ private native void nativeGeolocationPermissionsProvide(int nativeClass,
+ String origin, boolean allow, boolean remember);
/**
* Provide WebCore with the previously visted links from the history database
+ * @param nativeClass TODO
*/
- private native void nativeProvideVisitedHistory(String[] history);
+ private native void nativeProvideVisitedHistory(int nativeClass,
+ String[] history);
/**
* Modifies the current selection.
*
* Note: Accessibility support.
- *
+ * @param nativeClass Pointer to the C++ WebViewCore object mNativeClass
* @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(int direction, int granularity);
+ private native String nativeModifySelection(int nativeClass, int direction,
+ int granularity);
// EventHub for processing messages
private final EventHub mEventHub;
@@ -672,6 +711,7 @@ public final class WebViewCore {
private static final int REDUCE_PRIORITY = 1;
private static final int RESUME_PRIORITY = 2;
+ @Override
public void run() {
Looper.prepare();
Assert.assertNull(sWebCoreHandler);
@@ -720,6 +760,13 @@ public final class WebViewCore {
}
BrowserFrame.sJavaBridge.updateProxy((ProxyProperties)msg.obj);
break;
+
+ case EventHub.HEARTBEAT:
+ // Ping back the watchdog to let it know we're still processing
+ // messages.
+ Message m = (Message)msg.obj;
+ m.sendToTarget();
+ break;
}
}
};
@@ -814,6 +861,23 @@ public final class WebViewCore {
Rect mNativeLayerRect;
}
+ static class WebKitHitTest {
+ String mLinkUrl;
+ String mAnchorText;
+ String mImageUrl;
+ String mAltDisplayString;
+ String mTitle;
+ Rect[] mTouchRects;
+ boolean mEditable;
+ int mTapHighlightColor = WebView.HIGHLIGHT_COLOR;
+
+ // These are the input values that produced this hit test
+ int mHitTestX;
+ int mHitTestY;
+ int mHitTestSlop;
+ boolean mHitTestMovedMouse;
+ }
+
static class AutoFillData {
public AutoFillData() {
mQueryId = WebTextView.FORM_NOT_AUTOFILLABLE;
@@ -958,6 +1022,8 @@ public final class WebViewCore {
static final int SET_BACKGROUND_COLOR = 126;
static final int SET_MOVE_FOCUS = 127;
static final int SAVE_DOCUMENT_STATE = 128;
+ static final int DELETE_SURROUNDING_TEXT = 129;
+
static final int WEBKIT_DRAW = 130;
static final int POST_URL = 132;
@@ -1007,7 +1073,6 @@ public final class WebViewCore {
static final int DUMP_DOMTREE = 170;
static final int DUMP_RENDERTREE = 171;
static final int DUMP_NAVTREE = 172;
- static final int DUMP_V8COUNTERS = 173;
static final int SET_JS_FLAGS = 174;
static final int CONTENT_INVALIDATE_ALL = 175;
@@ -1025,7 +1090,7 @@ public 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 HIT_TEST = 187;
// accessibility support
static final int MODIFY_SELECTION = 190;
@@ -1042,9 +1107,18 @@ public final class WebViewCore {
static final int NOTIFY_ANIMATION_STARTED = 196;
+ static final int HEARTBEAT = 197;
+
+ static final int SCROLL_LAYER = 198;
+
// private message ids
private static final int DESTROY = 200;
+ // for cut & paste
+ static final int COPY_TEXT = 210;
+ static final int DELETE_TEXT = 211;
+ static final int INSERT_TEXT = 212;
+
// Private handler for WebCore messages.
private Handler mHandler;
// Message queue for containing messages before the WebCore thread is
@@ -1120,18 +1194,19 @@ public final class WebViewCore {
mSettings.onDestroyed();
mNativeClass = 0;
mWebView = null;
+ WebCoreThreadWatchdog.quit();
}
break;
case REVEAL_SELECTION:
- nativeRevealSelection();
+ nativeRevealSelection(mNativeClass);
break;
case REQUEST_LABEL:
if (mWebView != null) {
int nodePointer = msg.arg2;
- String label = nativeRequestLabel(msg.arg1,
- nodePointer);
+ String label = nativeRequestLabel(mNativeClass,
+ msg.arg1, nodePointer);
if (label != null && label.length() > 0) {
Message.obtain(mWebView.mPrivateHandler,
WebView.RETURN_LABEL, nodePointer,
@@ -1141,7 +1216,7 @@ public final class WebViewCore {
break;
case UPDATE_FRAME_CACHE_IF_LOADING:
- nativeUpdateFrameCacheIfLoading();
+ nativeUpdateFrameCacheIfLoading(mNativeClass);
break;
case SCROLL_TEXT_INPUT:
@@ -1151,7 +1226,7 @@ public final class WebViewCore {
} else {
xPercent = ((Float) msg.obj).floatValue();
}
- nativeScrollFocusedTextInput(xPercent, msg.arg2);
+ nativeScrollFocusedTextInput(mNativeClass, xPercent, msg.arg2);
break;
case LOAD_URL: {
@@ -1186,7 +1261,8 @@ public final class WebViewCore {
!scheme.startsWith("ftp") &&
!scheme.startsWith("about") &&
!scheme.startsWith("javascript")) {
- nativeRegisterURLSchemeAsLocal(scheme);
+ nativeRegisterURLSchemeAsLocal(mNativeClass,
+ scheme);
}
}
}
@@ -1195,7 +1271,7 @@ public final class WebViewCore {
loadParams.mMimeType,
loadParams.mEncoding,
loadParams.mHistoryUrl);
- nativeContentInvalidateAll();
+ nativeContentInvalidateAll(mNativeClass);
break;
case STOP_LOADING:
@@ -1224,11 +1300,11 @@ public final class WebViewCore {
break;
case FAKE_CLICK:
- nativeClick(msg.arg1, msg.arg2, true);
+ nativeClick(mNativeClass, msg.arg1, msg.arg2, true);
break;
case CLICK:
- nativeClick(msg.arg1, msg.arg2, false);
+ nativeClick(mNativeClass, msg.arg1, msg.arg2, false);
break;
case VIEW_SIZE_CHANGED: {
@@ -1239,14 +1315,14 @@ public final class WebViewCore {
// note: these are in document coordinates
// (inv-zoom)
Point pt = (Point) msg.obj;
- nativeSetScrollOffset(msg.arg1, msg.arg2 == 1,
- pt.x, pt.y);
+ nativeSetScrollOffset(mNativeClass, msg.arg1,
+ msg.arg2 == 1, pt.x, pt.y);
break;
case SET_GLOBAL_BOUNDS:
Rect r = (Rect) msg.obj;
- nativeSetGlobalBounds(r.left, r.top, r.width(),
- r.height());
+ nativeSetGlobalBounds(mNativeClass, r.left, r.top,
+ r.width(), r.height());
break;
case GO_BACK_FORWARD:
@@ -1275,7 +1351,7 @@ public final class WebViewCore {
WebViewWorker.getHandler().sendEmptyMessage(
WebViewWorker.MSG_PAUSE_CACHE_TRANSACTION);
} else {
- nativeCloseIdleConnections();
+ nativeCloseIdleConnections(mNativeClass);
}
break;
@@ -1289,16 +1365,16 @@ public final class WebViewCore {
break;
case ON_PAUSE:
- nativePause();
+ nativePause(mNativeClass);
break;
case ON_RESUME:
- nativeResume();
+ nativeResume(mNativeClass);
break;
case FREE_MEMORY:
clearCache(false);
- nativeFreeMemory();
+ nativeFreeMemory(mNativeClass);
break;
case SET_NETWORK_STATE:
@@ -1331,9 +1407,9 @@ public final class WebViewCore {
case REPLACE_TEXT:
ReplaceTextData rep = (ReplaceTextData) msg.obj;
- nativeReplaceTextfieldText(msg.arg1, msg.arg2,
- rep.mReplace, rep.mNewStart, rep.mNewEnd,
- rep.mTextGeneration);
+ nativeReplaceTextfieldText(mNativeClass, msg.arg1,
+ msg.arg2, rep.mReplace, rep.mNewStart,
+ rep.mNewEnd, rep.mTextGeneration);
break;
case PASS_TO_JS: {
@@ -1342,19 +1418,19 @@ public final class WebViewCore {
int keyCode = evt.getKeyCode();
int keyValue = evt.getUnicodeChar();
int generation = msg.arg1;
- passToJs(generation,
+ passToJs(mNativeClass,
+ generation,
jsData.mCurrentText,
keyCode,
keyValue,
- evt.isDown(),
- evt.isShiftPressed(), evt.isAltPressed(),
- evt.isSymPressed());
+ evt.isDown(), evt.isShiftPressed(),
+ evt.isAltPressed(), evt.isSymPressed());
break;
}
case SAVE_DOCUMENT_STATE: {
CursorData cDat = (CursorData) msg.obj;
- nativeSaveDocumentState(cDat.mFrame);
+ nativeSaveDocumentState(mNativeClass, cDat.mFrame);
break;
}
@@ -1363,7 +1439,7 @@ public final class WebViewCore {
// FIXME: This will not work for connections currently in use, as
// they cache the certificate responses. See http://b/5324235.
SslCertLookupTable.getInstance().clear();
- nativeCloseIdleConnections();
+ nativeCloseIdleConnections(mNativeClass);
} else {
Network.getInstance(mContext).clearUserSslPrefTable();
}
@@ -1372,10 +1448,12 @@ public final class WebViewCore {
case TOUCH_UP:
TouchUpData touchUpData = (TouchUpData) msg.obj;
if (touchUpData.mNativeLayer != 0) {
- nativeScrollLayer(touchUpData.mNativeLayer,
+ nativeScrollLayer(mNativeClass,
+ touchUpData.mNativeLayer,
touchUpData.mNativeLayerRect);
}
- nativeTouchUp(touchUpData.mMoveGeneration,
+ nativeTouchUp(mNativeClass,
+ touchUpData.mMoveGeneration,
touchUpData.mFrame, touchUpData.mNode,
touchUpData.mX, touchUpData.mY);
break;
@@ -1390,11 +1468,13 @@ public final class WebViewCore {
yArray[c] = ted.mPoints[c].y;
}
if (ted.mNativeLayer != 0) {
- nativeScrollLayer(ted.mNativeLayer,
- ted.mNativeLayerRect);
+ nativeScrollLayer(mNativeClass,
+ ted.mNativeLayer, ted.mNativeLayerRect);
}
- ted.mNativeResult = nativeHandleTouchEvent(ted.mAction, ted.mIds,
- xArray, yArray, count, ted.mActionIndex, ted.mMetaState);
+ ted.mNativeResult = nativeHandleTouchEvent(
+ mNativeClass, ted.mAction, ted.mIds, xArray,
+ yArray, count, ted.mActionIndex,
+ ted.mMetaState);
Message.obtain(
mWebView.mPrivateHandler,
WebView.PREVENT_TOUCH_ID,
@@ -1405,7 +1485,7 @@ public final class WebViewCore {
}
case SET_ACTIVE:
- nativeSetFocusControllerActive(msg.arg1 == 1);
+ nativeSetFocusControllerActive(mNativeClass, msg.arg1 == 1);
break;
case ADD_JS_INTERFACE:
@@ -1431,39 +1511,38 @@ public final class WebViewCore {
case SET_MOVE_FOCUS:
CursorData focusData = (CursorData) msg.obj;
- nativeMoveFocus(focusData.mFrame, focusData.mNode);
+ nativeMoveFocus(mNativeClass, focusData.mFrame, focusData.mNode);
break;
case SET_MOVE_MOUSE:
CursorData cursorData = (CursorData) msg.obj;
- nativeMoveMouse(cursorData.mFrame,
- cursorData.mX, cursorData.mY);
+ nativeMoveMouse(mNativeClass,
+ cursorData.mFrame, cursorData.mX, cursorData.mY);
break;
case SET_MOVE_MOUSE_IF_LATEST:
CursorData cData = (CursorData) msg.obj;
- nativeMoveMouseIfLatest(cData.mMoveGeneration,
- cData.mFrame,
- cData.mX, cData.mY);
+ nativeMoveMouseIfLatest(mNativeClass,
+ cData.mMoveGeneration,
+ cData.mFrame, cData.mX, cData.mY);
if (msg.arg1 == 1) {
- nativeStopPaintingCaret();
+ nativeStopPaintingCaret(mNativeClass);
}
break;
case REQUEST_CURSOR_HREF: {
+ WebKitHitTest hit = performHitTest(msg.arg1, msg.arg2, 1, false);
Message hrefMsg = (Message) msg.obj;
- hrefMsg.getData().putString("url",
- nativeRetrieveHref(msg.arg1, msg.arg2));
- hrefMsg.getData().putString("title",
- nativeRetrieveAnchorText(msg.arg1, msg.arg2));
- hrefMsg.getData().putString("src",
- nativeRetrieveImageSource(msg.arg1, msg.arg2));
+ Bundle data = hrefMsg.getData();
+ data.putString(FocusNodeHref.URL,hit.mLinkUrl);
+ data.putString(FocusNodeHref.TITLE, hit.mAnchorText);
+ data.putString(FocusNodeHref.SRC, hit.mImageUrl);
hrefMsg.sendToTarget();
break;
}
case UPDATE_CACHE_AND_TEXT_ENTRY:
- nativeUpdateFrameCache();
+ nativeUpdateFrameCache(mNativeClass);
// FIXME: this should provide a minimal rectangle
if (mWebView != null) {
mWebView.postInvalidate();
@@ -1481,17 +1560,18 @@ public final class WebViewCore {
case DELETE_SELECTION:
TextSelectionData deleteSelectionData
= (TextSelectionData) msg.obj;
- nativeDeleteSelection(deleteSelectionData.mStart,
- deleteSelectionData.mEnd, msg.arg1);
+ nativeDeleteSelection(mNativeClass,
+ deleteSelectionData.mStart, deleteSelectionData.mEnd, msg.arg1);
break;
case SET_SELECTION:
- nativeSetSelection(msg.arg1, msg.arg2);
+ nativeSetSelection(mNativeClass, msg.arg1, msg.arg2);
break;
case MODIFY_SELECTION:
- String modifiedSelectionString = nativeModifySelection(msg.arg1,
- msg.arg2);
+ String modifiedSelectionString =
+ nativeModifySelection(mNativeClass, msg.arg1,
+ msg.arg2);
mWebView.mPrivateHandler.obtainMessage(WebView.SELECTION_STRING_CHANGED,
modifiedSelectionString).sendToTarget();
break;
@@ -1504,40 +1584,36 @@ public final class WebViewCore {
for (int c = 0; c < choicesSize; c++) {
choicesArray[c] = choices.get(c);
}
- nativeSendListBoxChoices(choicesArray,
- choicesSize);
+ nativeSendListBoxChoices(mNativeClass,
+ choicesArray, choicesSize);
break;
case SINGLE_LISTBOX_CHOICE:
- nativeSendListBoxChoice(msg.arg1);
+ nativeSendListBoxChoice(mNativeClass, msg.arg1);
break;
case SET_BACKGROUND_COLOR:
- nativeSetBackgroundColor(msg.arg1);
+ nativeSetBackgroundColor(mNativeClass, msg.arg1);
break;
case DUMP_DOMTREE:
- nativeDumpDomTree(msg.arg1 == 1);
+ nativeDumpDomTree(mNativeClass, msg.arg1 == 1);
break;
case DUMP_RENDERTREE:
- nativeDumpRenderTree(msg.arg1 == 1);
+ nativeDumpRenderTree(mNativeClass, msg.arg1 == 1);
break;
case DUMP_NAVTREE:
- nativeDumpNavTree();
- break;
-
- case DUMP_V8COUNTERS:
- nativeDumpV8Counters();
+ nativeDumpNavTree(mNativeClass);
break;
case SET_JS_FLAGS:
- nativeSetJsFlags((String)msg.obj);
+ nativeSetJsFlags(mNativeClass, (String)msg.obj);
break;
case CONTENT_INVALIDATE_ALL:
- nativeContentInvalidateAll();
+ nativeContentInvalidateAll(mNativeClass);
break;
case SAVE_WEBARCHIVE:
@@ -1552,12 +1628,12 @@ public final class WebViewCore {
case GEOLOCATION_PERMISSIONS_PROVIDE:
GeolocationPermissionsData data =
(GeolocationPermissionsData) msg.obj;
- nativeGeolocationPermissionsProvide(data.mOrigin,
- data.mAllow, data.mRemember);
+ nativeGeolocationPermissionsProvide(mNativeClass,
+ data.mOrigin, data.mAllow, data.mRemember);
break;
case SPLIT_PICTURE_SET:
- nativeSplitContent(msg.arg1);
+ nativeSplitContent(mNativeClass, msg.arg1);
mWebView.mPrivateHandler.obtainMessage(
WebView.REPLACE_BASE_CONTENT, msg.arg1, 0);
mSplitPictureIsScheduled = false;
@@ -1567,7 +1643,7 @@ public final class WebViewCore {
// Clear the view so that onDraw() will draw nothing
// but white background
// (See public method WebView.clearView)
- nativeClearContent();
+ clearContent();
break;
case MESSAGE_RELAY:
@@ -1575,15 +1651,15 @@ public final class WebViewCore {
break;
case POPULATE_VISITED_LINKS:
- nativeProvideVisitedHistory((String[])msg.obj);
+ nativeProvideVisitedHistory(mNativeClass, (String[])msg.obj);
break;
case VALID_NODE_BOUNDS: {
MotionUpData motionUpData = (MotionUpData) msg.obj;
if (!nativeValidNodeAndBounds(
- motionUpData.mFrame, motionUpData.mNode,
- motionUpData.mBounds)) {
- nativeUpdateFrameCache();
+ mNativeClass, motionUpData.mFrame,
+ motionUpData.mNode, motionUpData.mBounds)) {
+ nativeUpdateFrameCache(mNativeClass);
}
Message message = mWebView.mPrivateHandler
.obtainMessage(WebView.DO_MOTION_UP,
@@ -1594,11 +1670,11 @@ public final class WebViewCore {
}
case HIDE_FULLSCREEN:
- nativeFullScreenPluginHidden(msg.arg1);
+ nativeFullScreenPluginHidden(mNativeClass, msg.arg1);
break;
case PLUGIN_SURFACE_READY:
- nativePluginSurfaceReady();
+ nativePluginSurfaceReady(mNativeClass);
break;
case NOTIFY_ANIMATION_STARTED:
@@ -1614,16 +1690,15 @@ public final class WebViewCore {
(Set<String>) msg.obj);
break;
- case GET_TOUCH_HIGHLIGHT_RECTS:
+ case HIT_TEST:
TouchHighlightData d = (TouchHighlightData) msg.obj;
if (d.mNativeLayer != 0) {
- nativeScrollLayer(d.mNativeLayer,
- d.mNativeLayerRect);
+ nativeScrollLayer(mNativeClass,
+ d.mNativeLayer, d.mNativeLayerRect);
}
- ArrayList<Rect> rects = nativeGetTouchHighlightRects
- (d.mX, d.mY, d.mSlop);
+ WebKitHitTest hit = performHitTest(d.mX, d.mY, d.mSlop, true);
mWebView.mPrivateHandler.obtainMessage(
- WebView.SET_TOUCH_HIGHLIGHT_RECTS, rects)
+ WebView.HIT_TEST_RESULT, hit)
.sendToTarget();
break;
@@ -1632,7 +1707,7 @@ public final class WebViewCore {
break;
case AUTOFILL_FORM:
- nativeAutoFillForm(msg.arg1);
+ nativeAutoFillForm(mNativeClass, msg.arg1);
mWebView.mPrivateHandler.obtainMessage(WebView.AUTOFILL_COMPLETE, null)
.sendToTarget();
break;
@@ -1645,6 +1720,33 @@ public final class WebViewCore {
mBrowserFrame.stringByEvaluatingJavaScriptFromString((String) msg.obj);
}
break;
+ case SCROLL_LAYER:
+ int nativeLayer = msg.arg1;
+ Rect rect = (Rect) msg.obj;
+ nativeScrollLayer(mNativeClass, nativeLayer,
+ rect);
+ break;
+
+ case DELETE_TEXT: {
+ int[] handles = (int[]) msg.obj;
+ nativeDeleteText(mNativeClass, handles[0],
+ handles[1], handles[2], handles[3]);
+ break;
+ }
+ case COPY_TEXT: {
+ int[] handles = (int[]) msg.obj;
+ String copiedText = nativeGetText(mNativeClass,
+ handles[0], handles[1], handles[2],
+ handles[3]);
+ if (copiedText != null) {
+ mWebView.mPrivateHandler.obtainMessage(WebView.COPY_TO_CLIPBOARD, copiedText)
+ .sendToTarget();
+ }
+ break;
+ }
+ case INSERT_TEXT:
+ nativeInsertText(mNativeClass, (String) msg.obj);
+ break;
}
}
};
@@ -1820,6 +1922,15 @@ public final class WebViewCore {
// WebViewCore private methods
//-------------------------------------------------------------------------
+ private WebKitHitTest performHitTest(int x, int y, int slop, boolean moveMouse) {
+ WebKitHitTest hit = nativeHitTest(mNativeClass, x, y, slop, moveMouse);
+ hit.mHitTestX = x;
+ hit.mHitTestY = y;
+ hit.mHitTestSlop = slop;
+ hit.mHitTestMovedMouse = moveMouse;
+ return hit;
+ }
+
private void clearCache(boolean includeDiskFiles) {
mBrowserFrame.clearCache();
if (includeDiskFiles) {
@@ -1853,9 +1964,9 @@ public final class WebViewCore {
unicodeChar = evt.getCharacters().codePointAt(0);
}
- if (!nativeKey(keyCode, unicodeChar, evt.getRepeatCount(), evt.isShiftPressed(),
- evt.isAltPressed(), evt.isSymPressed(),
- isDown) && keyCode != KeyEvent.KEYCODE_ENTER) {
+ if (!nativeKey(mNativeClass, keyCode, unicodeChar, evt.getRepeatCount(),
+ evt.isShiftPressed(), evt.isAltPressed(),
+ evt.isSymPressed(), isDown) && keyCode != KeyEvent.KEYCODE_ENTER) {
if (keyCode >= KeyEvent.KEYCODE_DPAD_UP
&& keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) {
if (DebugFlags.WEB_VIEW_CORE) {
@@ -1901,9 +2012,9 @@ public final class WebViewCore {
float ratio = (heightWidthRatio > 0) ? heightWidthRatio : (float) h / w;
height = Math.round(ratio * width);
}
- nativeSetSize(width, height, textwrapWidth, scale, w,
- data.mActualViewHeight > 0 ? data.mActualViewHeight : h,
- data.mAnchorX, data.mAnchorY, data.mIgnoreHeight);
+ int screenHeight = data.mActualViewHeight > 0 ? data.mActualViewHeight : h;
+ nativeSetSize(mNativeClass, width, height, textwrapWidth, scale,
+ w, screenHeight, data.mAnchorX, data.mAnchorY, data.mIgnoreHeight);
// Remember the current width and height
boolean needInvalidate = (mCurrentViewWidth == 0);
mCurrentViewWidth = w;
@@ -2039,7 +2150,8 @@ public final class WebViewCore {
mDrawIsScheduled = false;
DrawData draw = new DrawData();
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start");
- draw.mBaseLayer = nativeRecordContent(draw.mInvalRegion, draw.mContentSize);
+ draw.mBaseLayer = nativeRecordContent(mNativeClass, draw.mInvalRegion,
+ draw.mContentSize);
if (draw.mBaseLayer == 0) {
if (mWebView != null && !mWebView.isPaused()) {
if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort, resending draw message");
@@ -2055,14 +2167,14 @@ public final class WebViewCore {
private void webkitDraw(DrawData draw) {
if (mWebView != null) {
- draw.mFocusSizeChanged = nativeFocusBoundsChanged();
+ draw.mFocusSizeChanged = nativeFocusBoundsChanged(mNativeClass);
draw.mViewSize = new Point(mCurrentViewWidth, mCurrentViewHeight);
if (mSettings.getUseWideViewPort()) {
draw.mMinPrefWidth = Math.max(
mViewportWidth == -1 ? WebView.DEFAULT_VIEWPORT_WIDTH
: (mViewportWidth == 0 ? mCurrentViewWidth
: mViewportWidth),
- nativeGetContentMinPrefWidth());
+ nativeGetContentMinPrefWidth(mNativeClass));
}
if (mInitialViewState != null) {
draw.mViewState = mInitialViewState;
@@ -2113,7 +2225,7 @@ public final class WebViewCore {
Log.w(LOGTAG, "Cannot pauseUpdatePicture, core destroyed or not initialized!");
return;
}
- core.nativeSetIsPaused(true);
+ core.nativeSetIsPaused(core.mNativeClass, true);
core.mDrawIsPaused = true;
}
}
@@ -2131,7 +2243,7 @@ public final class WebViewCore {
Log.w(LOGTAG, "Cannot resumeUpdatePicture, core destroyed!");
return;
}
- core.nativeSetIsPaused(false);
+ core.nativeSetIsPaused(core.mNativeClass, false);
core.mDrawIsPaused = false;
// always redraw on resume to reenable gif animations
core.mDrawIsScheduled = false;
@@ -2255,7 +2367,7 @@ public final class WebViewCore {
return mWebView;
}
- private native void setViewportSettingsFromNative();
+ private native void setViewportSettingsFromNative(int nativeClass);
// called by JNI
private void didFirstLayout(boolean standardLoad) {
@@ -2277,9 +2389,9 @@ public final class WebViewCore {
}
// remove the touch highlight when moving to a new page
- if (WebView.USE_WEBKIT_RINGS || getSettings().supportTouchOnly()) {
+ if (WebView.sDisableNavcache) {
mWebView.mPrivateHandler.sendEmptyMessage(
- WebView.SET_TOUCH_HIGHLIGHT_RECTS);
+ WebView.HIT_TEST_RESULT);
}
// reset the scroll position, the restored offset and scales
@@ -2300,7 +2412,7 @@ public final class WebViewCore {
return;
}
// set the viewport settings from WebKit
- setViewportSettingsFromNative();
+ setViewportSettingsFromNative(mNativeClass);
// clamp initial scale
if (mViewportInitialScale > 0) {
@@ -2338,11 +2450,7 @@ public final class WebViewCore {
// adjust the default scale to match the densityDpi
float adjust = 1.0f;
if (mViewportDensityDpi == -1) {
- // 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();
- }
+ adjust = mContext.getResources().getDisplayMetrics().density;
} else if (mViewportDensityDpi > 0) {
adjust = (float) mContext.getResources().getDisplayMetrics().densityDpi
/ mViewportDensityDpi;
@@ -2521,7 +2629,7 @@ public final class WebViewCore {
if (mSettings.isNarrowColumnLayout()) {
// In case of automatic text reflow in fixed view port mode.
mInitialViewState.mTextWrapScale =
- mWebView.getReadingLevelScale();
+ mWebView.computeReadingLevelScale(data.mScale);
}
} else {
// Scale is given such as when page is restored, use it.
@@ -2614,18 +2722,31 @@ public final class WebViewCore {
WebView.FIND_AGAIN).sendToTarget();
}
- private native void nativeUpdateFrameCacheIfLoading();
- private native void nativeRevealSelection();
- private native String nativeRequestLabel(int framePtr, int nodePtr);
+ // called by JNI
+ private void initEditField(String text, int start, int end) {
+ if (mWebView == null) {
+ return;
+ }
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.INIT_EDIT_FIELD, start, end, text).sendToTarget();
+ }
+
+ private native void nativeUpdateFrameCacheIfLoading(int nativeClass);
+ private native void nativeRevealSelection(int nativeClass);
+ private native String nativeRequestLabel(int nativeClass, int framePtr,
+ int nodePtr);
/**
* Scroll the focused textfield to (xPercent, y) in document space
*/
- private native void nativeScrollFocusedTextInput(float xPercent, int y);
+ private native void nativeScrollFocusedTextInput(int nativeClass,
+ float xPercent, int y);
// these must be in document space (i.e. not scaled/zoomed).
- private native void nativeSetScrollOffset(int gen, boolean sendScrollEvent, int dx, int dy);
+ private native void nativeSetScrollOffset(int nativeClass, int gen,
+ boolean sendScrollEvent, int dx, int dy);
- private native void nativeSetGlobalBounds(int x, int y, int w, int h);
+ private native void nativeSetGlobalBounds(int nativeClass, int x, int y,
+ int w, int h);
// called by JNI
private void requestListBox(String[] array, int[] enabledArray,
@@ -2860,18 +2981,49 @@ public final class WebViewCore {
return mDeviceOrientationService;
}
- private native void nativeSetIsPaused(boolean isPaused);
- private native void nativePause();
- private native void nativeResume();
- private native void nativeFreeMemory();
- private native void nativeFullScreenPluginHidden(int npp);
- private native void nativePluginSurfaceReady();
- private native boolean nativeValidNodeAndBounds(int frame, int node,
- Rect bounds);
+ private native void nativeSetIsPaused(int nativeClass, boolean isPaused);
+ private native void nativePause(int nativeClass);
+ private native void nativeResume(int nativeClass);
+ private native void nativeFreeMemory(int nativeClass);
+ private native void nativeFullScreenPluginHidden(int nativeClass, int npp);
+ private native void nativePluginSurfaceReady(int nativeClass);
+ private native boolean nativeValidNodeAndBounds(int nativeClass, int frame,
+ int node, Rect bounds);
- private native ArrayList<Rect> nativeGetTouchHighlightRects(int x, int y,
- int slop);
+ private native WebKitHitTest nativeHitTest(int nativeClass, int x, int y,
+ int slop, boolean moveMouse);
- private native void nativeAutoFillForm(int queryId);
- private native void nativeScrollLayer(int layer, Rect rect);
+ private native void nativeAutoFillForm(int nativeClass, int queryId);
+ private native void nativeScrollLayer(int nativeClass, int layer, Rect rect);
+
+ /**
+ * Deletes editable text between two points. Note that the selection may
+ * differ from the WebView's selection because the algorithms for selecting
+ * text differs for non-LTR text. Any text that isn't editable will be
+ * left unchanged.
+ * @param nativeClass The pointer to the native class (mNativeClass)
+ * @param startX The X position of the top-left selection point.
+ * @param startY The Y position of the top-left selection point.
+ * @param endX The X position of the bottom-right selection point.
+ * @param endY The Y position of the bottom-right selection point.
+ */
+ private native void nativeDeleteText(int nativeClass,
+ int startX, int startY, int endX, int endY);
+ /**
+ * Inserts text at the current cursor position. If the currently-focused
+ * node does not have a cursor position then this function does nothing.
+ */
+ private native void nativeInsertText(int nativeClass, String text);
+ /**
+ * Gets the text between two selection points. Note that the selection
+ * may differ from the WebView's selection because the algorithms for
+ * selecting text differs for non-LTR text.
+ * @param nativeClass The pointer to the native class (mNativeClass)
+ * @param startX The X position of the top-left selection point.
+ * @param startY The Y position of the top-left selection point.
+ * @param endX The X position of the bottom-right selection point.
+ * @param endY The Y position of the bottom-right selection point.
+ */
+ private native String nativeGetText(int nativeClass,
+ int startX, int startY, int endX, int endY);
}
diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java
index 14bdc42..8ffba64 100644
--- a/core/java/android/webkit/ZoomManager.java
+++ b/core/java/android/webkit/ZoomManager.java
@@ -316,7 +316,12 @@ class ZoomManager {
* Returns the zoom scale used for reading text on a double-tap.
*/
public final float getReadingLevelScale() {
- return mDisplayDensity * mDoubleTapZoomFactor;
+ return computeScaleWithLimits(computeReadingLevelScale(getZoomOverviewScale()));
+ }
+
+ /* package */ final float computeReadingLevelScale(float scale) {
+ return Math.max(mDisplayDensity * mDoubleTapZoomFactor,
+ scale + MIN_DOUBLE_TAP_SCALE_INCREMENT);
}
public final float getInvDefaultScale() {
@@ -678,7 +683,7 @@ class ZoomManager {
}
zoomToOverview();
} else {
- zoomToReadingLevelOrMore();
+ zoomToReadingLevel();
}
}
@@ -709,9 +714,8 @@ class ZoomManager {
!mWebView.getSettings().getUseFixedViewport());
}
- private void zoomToReadingLevelOrMore() {
- final float zoomScale = Math.max(getReadingLevelScale(),
- mActualScale + MIN_DOUBLE_TAP_SCALE_INCREMENT);
+ private void zoomToReadingLevel() {
+ final float readingScale = getReadingLevelScale();
int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale);
if (left != WebView.NO_LEFTEDGE) {
@@ -721,13 +725,13 @@ class ZoomManager {
// Re-calculate the zoom center so that the new scroll x will be
// on the left edge.
if (viewLeft > 0) {
- mZoomCenterX = viewLeft * zoomScale / (zoomScale - mActualScale);
+ mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale);
} else {
mWebView.scrollBy(viewLeft, 0);
mZoomCenterX = 0;
}
}
- startZoomAnimation(zoomScale,
+ startZoomAnimation(readingScale,
!mWebView.getSettings().getUseFixedViewport());
}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 38bb2e1..e94b1cb 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -20,7 +20,6 @@ import com.android.internal.R;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
@@ -1297,6 +1296,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
super.sendAccessibilityEvent(eventType);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(AbsListView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(AbsListView.class.getName());
+ }
+
/**
* Indicates whether the children's drawing cache is used during a scroll.
* By default, the drawing cache is enabled but this will consume more memory.
@@ -5572,6 +5583,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
/**
+ * Hints the RemoteViewsAdapter, if it exists, about which views are currently
+ * being displayed by the AbsListView.
+ */
+ void setVisibleRangeHint(int start, int end) {
+ if (mRemoteAdapter != null) {
+ mRemoteAdapter.setVisibleRangeHint(start, end);
+ }
+ }
+
+ /**
* Sets the recycler listener to be notified whenever a View is set aside in
* the recycler for later reuse. This listener can be used to free resources
* associated to the View.
diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java
index bdaf89e..e36afa3 100644
--- a/core/java/android/widget/AbsSeekBar.java
+++ b/core/java/android/widget/AbsSeekBar.java
@@ -25,6 +25,8 @@ import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
public abstract class AbsSeekBar extends ProgressBar {
private Drawable mThumb;
@@ -464,4 +466,15 @@ public abstract class AbsSeekBar extends ProgressBar {
return super.onKeyDown(keyCode, event);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(AbsSeekBar.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(AbsSeekBar.class.getName());
+ }
}
diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java
index 3d79205..efdfae3 100644
--- a/core/java/android/widget/AbsSpinner.java
+++ b/core/java/android/widget/AbsSpinner.java
@@ -28,6 +28,8 @@ import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
* An abstract base class for spinner widgets. SDK users will probably not
@@ -40,7 +42,6 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
int mHeightMeasureSpec;
int mWidthMeasureSpec;
- boolean mBlockLayoutRequests;
int mSelectionLeftPadding = 0;
int mSelectionTopPadding = 0;
@@ -463,4 +464,16 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
scrapHeap.clear();
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(AbsSpinner.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(AbsSpinner.class.getName());
+ }
}
diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java
index 60b24bc..be6b4e2 100644
--- a/core/java/android/widget/ActivityChooserView.java
+++ b/core/java/android/widget/ActivityChooserView.java
@@ -33,8 +33,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
import android.widget.ActivityChooserModel.ActivityChooserModelClient;
/**
@@ -366,7 +364,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod
getListPopupWindow().dismiss();
ViewTreeObserver viewTreeObserver = getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
- viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
+ viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
}
return true;
@@ -400,7 +398,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod
}
ViewTreeObserver viewTreeObserver = getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
- viewTreeObserver.removeGlobalOnLayoutListener(mOnGlobalLayoutListener);
+ viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
mIsAttachedToWindow = false;
}
@@ -547,6 +545,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod
position = mAdapter.getShowDefaultActivity() ? position : position + 1;
Intent launchIntent = mAdapter.getDataModel().chooseActivity(position);
if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(launchIntent);
}
}
@@ -564,6 +563,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod
final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(launchIntent);
}
} else if (view == mExpandActivityOverflowButton) {
diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java
index 40df168..97a864c 100644
--- a/core/java/android/widget/AdapterView.java
+++ b/core/java/android/widget/AdapterView.java
@@ -913,6 +913,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup {
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(AdapterView.class.getName());
info.setScrollable(isScrollableForAccessibility());
View selectedView = getSelectedView();
if (selectedView != null) {
@@ -923,6 +924,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup {
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(AdapterView.class.getName());
event.setScrollable(isScrollableForAccessibility());
View selectedView = getSelectedView();
if (selectedView != null) {
diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java
index c83c780..bb00049 100644
--- a/core/java/android/widget/AdapterViewAnimator.java
+++ b/core/java/android/widget/AdapterViewAnimator.java
@@ -29,6 +29,8 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import java.util.ArrayList;
import java.util.HashMap;
@@ -555,6 +557,11 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter>
mCurrentWindowStart = newWindowStart;
mCurrentWindowEnd = newWindowEnd;
mCurrentWindowStartUnbounded = newWindowStartUnbounded;
+ if (mRemoteViewsAdapter != null) {
+ int adapterStart = modulo(mCurrentWindowStart, adapterCount);
+ int adapterEnd = modulo(mCurrentWindowEnd, adapterCount);
+ mRemoteViewsAdapter.setVisibleRangeHint(adapterStart, adapterEnd);
+ }
}
requestLayout();
invalidate();
@@ -1045,4 +1052,16 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter>
*/
public void fyiWillBeAdvancedByHostKThx() {
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(AdapterViewAnimator.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(AdapterViewAnimator.class.getName());
+ }
}
diff --git a/core/java/android/widget/AdapterViewFlipper.java b/core/java/android/widget/AdapterViewFlipper.java
index 4419886..5096227 100644
--- a/core/java/android/widget/AdapterViewFlipper.java
+++ b/core/java/android/widget/AdapterViewFlipper.java
@@ -16,7 +16,6 @@
package android.widget;
-import android.animation.ObjectAnimator;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -27,7 +26,8 @@ import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.RemotableViewMethod;
-import android.view.animation.AlphaAnimation;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
/**
@@ -268,4 +268,16 @@ public class AdapterViewFlipper extends AdapterViewAnimator {
mAdvancedByHost = true;
updateRunning(false);
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(AdapterViewFlipper.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(AdapterViewFlipper.class.getName());
+ }
}
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
index 07523e3..f7a6b27 100644
--- a/core/java/android/widget/AutoCompleteTextView.java
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -1085,10 +1085,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
for (int i = 0; i < count; i++) {
if (adapter.isEnabled(i)) {
- realCount++;
Object item = adapter.getItem(i);
long id = adapter.getItemId(i);
- completions[i] = new CompletionInfo(id, i, convertSelectionToString(item));
+ completions[realCount] = new CompletionInfo(id, realCount,
+ convertSelectionToString(item));
+ realCount++;
}
}
diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java
index 8d58a6d..99f4cae 100644
--- a/core/java/android/widget/Button.java
+++ b/core/java/android/widget/Button.java
@@ -18,9 +18,8 @@ package android.widget;
import android.content.Context;
import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
@@ -107,4 +106,16 @@ public class Button extends TextView {
public Button(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Button.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Button.class.getName());
+ }
}
diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java
index e0403ff..85252af 100644
--- a/core/java/android/widget/CalendarView.java
+++ b/core/java/android/widget/CalendarView.java
@@ -39,6 +39,8 @@ import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AbsListView.OnScrollListener;
import com.android.internal.R;
@@ -431,6 +433,18 @@ public class CalendarView extends FrameLayout {
setCurrentLocale(newConfig.locale);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(CalendarView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(CalendarView.class.getName());
+ }
+
/**
* Gets the minimal date supported by this {@link CalendarView} in milliseconds
* since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java
index 2788846..0685eea 100644
--- a/core/java/android/widget/CheckBox.java
+++ b/core/java/android/widget/CheckBox.java
@@ -19,6 +19,7 @@ package android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.R;
@@ -78,4 +79,16 @@ public class CheckBox extends CompoundButton {
event.getText().add(mContext.getString(R.string.checkbox_not_checked));
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(CheckBox.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(CheckBox.class.getName());
+ }
}
diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java
index 0a54743..5c7e5a3 100644
--- a/core/java/android/widget/CheckedTextView.java
+++ b/core/java/android/widget/CheckedTextView.java
@@ -220,6 +220,7 @@ public class CheckedTextView extends TextView implements Checkable {
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(CheckedTextView.class.getName());
event.setChecked(mChecked);
}
@@ -236,6 +237,7 @@ public class CheckedTextView extends TextView implements Checkable {
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(CheckedTextView.class.getName());
info.setChecked(mChecked);
}
}
diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java
index 7e66722..0370049 100644
--- a/core/java/android/widget/Chronometer.java
+++ b/core/java/android/widget/Chronometer.java
@@ -25,6 +25,8 @@ import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
import java.util.Formatter;
@@ -276,4 +278,16 @@ public class Chronometer extends TextView {
mOnChronometerTickListener.onChronometerTick(this);
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Chronometer.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Chronometer.class.getName());
+ }
}
diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java
index d3cdad8..02c4c4f 100644
--- a/core/java/android/widget/CompoundButton.java
+++ b/core/java/android/widget/CompoundButton.java
@@ -211,12 +211,14 @@ public abstract class CompoundButton extends Button implements Checkable {
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(CompoundButton.class.getName());
event.setChecked(mChecked);
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(CompoundButton.class.getName());
info.setCheckable(true);
info.setChecked(mChecked);
}
diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java
index 0f462ff..110c8f3 100644
--- a/core/java/android/widget/DatePicker.java
+++ b/core/java/android/widget/DatePicker.java
@@ -31,6 +31,7 @@ import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.NumberPicker.OnValueChangeListener;
@@ -391,6 +392,18 @@ public class DatePicker extends FrameLayout {
}
@Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(DatePicker.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(DatePicker.class.getName());
+ }
+
+ @Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setCurrentLocale(newConfig.locale);
diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java
index 379883a..add9d9b 100644
--- a/core/java/android/widget/DigitalClock.java
+++ b/core/java/android/widget/DigitalClock.java
@@ -24,6 +24,8 @@ import android.os.SystemClock;
import android.provider.Settings;
import android.text.format.DateFormat;
import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import java.util.Calendar;
@@ -126,4 +128,16 @@ public class DigitalClock extends TextView {
setFormat();
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(DigitalClock.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(DigitalClock.class.getName());
+ }
}
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index 0da68a4..2fd8768 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -24,6 +24,8 @@ import android.text.TextUtils;
import android.text.method.ArrowKeyMovementMethod;
import android.text.method.MovementMethod;
import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/*
@@ -114,4 +116,16 @@ public class EditText extends TextView {
}
super.setEllipsize(ellipsis);
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(EditText.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(EditText.class.getName());
+ }
}
diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java
index ead9b4f..badfaa7 100644
--- a/core/java/android/widget/ExpandableListView.java
+++ b/core/java/android/widget/ExpandableListView.java
@@ -30,6 +30,8 @@ import android.view.ContextMenu;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ExpandableListConnector.PositionMetadata;
import java.util.ArrayList;
@@ -1167,4 +1169,15 @@ public class ExpandableListView extends ListView {
}
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ExpandableListView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ExpandableListView.class.getName());
+ }
}
diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java
index 74a57b0..da98884 100644
--- a/core/java/android/widget/FrameLayout.java
+++ b/core/java/android/widget/FrameLayout.java
@@ -29,6 +29,8 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
@@ -555,6 +557,19 @@ public class FrameLayout extends ViewGroup {
return new LayoutParams(p);
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(FrameLayout.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(FrameLayout.class.getName());
+ }
+
/**
* Per-child layout information for layouts that support margins.
* See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java
index 5e37fa8..03fdc39 100644
--- a/core/java/android/widget/Gallery.java
+++ b/core/java/android/widget/Gallery.java
@@ -32,6 +32,8 @@ import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Transformation;
import com.android.internal.R;
@@ -1355,6 +1357,18 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Gallery.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Gallery.class.getName());
+ }
+
/**
* Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to
* initiate a fling. Each frame of the fling is handled in {@link #run()}.
diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java
index 7cf5168..984ec79 100644
--- a/core/java/android/widget/GridLayout.java
+++ b/core/java/android/widget/GridLayout.java
@@ -27,6 +27,9 @@ import android.util.Pair;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
import com.android.internal.R;
import java.lang.reflect.Array;
@@ -839,9 +842,11 @@ public class GridLayout extends ViewGroup {
* @hide
*/
@Override
- protected void onChildVisibilityChanged(View child, int visibility) {
- super.onChildVisibilityChanged(child, visibility);
- invalidateStructure();
+ protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
+ super.onChildVisibilityChanged(child, oldVisibility, newVisibility);
+ if (oldVisibility == GONE || newVisibility == GONE) {
+ invalidateStructure();
+ }
}
// Measurement
@@ -1041,6 +1046,18 @@ public class GridLayout extends ViewGroup {
}
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(GridLayout.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(GridLayout.class.getName());
+ }
+
// Inner classes
/*
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index 5d406de..be2df8e 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -27,6 +27,8 @@ import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.GridLayoutAnimationController;
import android.widget.RemoteViews.RemoteView;
@@ -290,6 +292,7 @@ public class GridView extends AbsListView {
pos += mNumColumns;
}
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
@@ -382,6 +385,7 @@ public class GridView extends AbsListView {
mFirstPosition = Math.max(0, pos + 1);
}
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
@@ -2116,5 +2120,16 @@ public class GridView extends AbsListView {
}
return result;
}
-}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(GridView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(GridView.class.getName());
+ }
+}
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 1683d20..0b4ebf4 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -721,12 +721,14 @@ public class HorizontalScrollView extends FrameLayout {
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(HorizontalScrollView.class.getName());
info.setScrollable(getScrollRange() > 0);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(HorizontalScrollView.class.getName());
event.setScrollable(getScrollRange() > 0);
event.setScrollX(mScrollX);
event.setScrollY(mScrollY);
diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java
index d680fad..59a8f28 100644
--- a/core/java/android/widget/ImageButton.java
+++ b/core/java/android/widget/ImageButton.java
@@ -21,6 +21,8 @@ import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
import java.util.Map;
@@ -90,4 +92,16 @@ public class ImageButton extends ImageView {
protected boolean onSetAlpha(int alpha) {
return false;
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ImageButton.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ImageButton.class.getName());
+ }
}
diff --git a/core/java/android/widget/ImageSwitcher.java b/core/java/android/widget/ImageSwitcher.java
index bcb750a..c048970 100644
--- a/core/java/android/widget/ImageSwitcher.java
+++ b/core/java/android/widget/ImageSwitcher.java
@@ -16,12 +16,12 @@
package android.widget;
-import java.util.Map;
-
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
public class ImageSwitcher extends ViewSwitcher
@@ -55,5 +55,16 @@ public class ImageSwitcher extends ViewSwitcher
image.setImageDrawable(drawable);
showNext();
}
-}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ImageSwitcher.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ImageSwitcher.class.getName());
+ }
+}
diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java
index 73e1273..07ae93b 100644
--- a/core/java/android/widget/ImageView.java
+++ b/core/java/android/widget/ImageView.java
@@ -37,6 +37,7 @@ import android.view.RemotableViewMethod;
import android.view.View;
import android.view.ViewDebug;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
/**
@@ -1060,4 +1061,16 @@ public class ImageView extends View {
mDrawable.setVisible(false, false);
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ImageView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ImageView.class.getName());
+ }
}
diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java
index 427fd3e..b5deec7 100644
--- a/core/java/android/widget/LinearLayout.java
+++ b/core/java/android/widget/LinearLayout.java
@@ -27,6 +27,8 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
@@ -1729,7 +1731,19 @@ public class LinearLayout extends ViewGroup {
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LinearLayout.LayoutParams;
}
-
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(LinearLayout.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(LinearLayout.class.getName());
+ }
+
/**
* Per-child layout information associated with ViewLinearLayout.
*
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 7f7a3a7..e20d12a 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -32,13 +32,13 @@ import android.util.AttributeSet;
import android.util.SparseBooleanArray;
import android.view.FocusFinder;
import android.view.KeyEvent;
-import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
import java.util.ArrayList;
@@ -678,6 +678,7 @@ public class ListView extends AbsListView {
pos++;
}
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
@@ -711,7 +712,7 @@ public class ListView extends AbsListView {
}
mFirstPosition = pos + 1;
-
+ setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
@@ -3609,4 +3610,16 @@ public class ListView extends AbsListView {
}
return new long[0];
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ListView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ListView.class.getName());
+ }
}
diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java
index f2ea3fc..fc35f05 100644
--- a/core/java/android/widget/MediaController.java
+++ b/core/java/android/widget/MediaController.java
@@ -31,6 +31,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.SeekBar.OnSeekBarChangeListener;
import com.android.internal.policy.PolicyManager;
@@ -592,6 +594,18 @@ public class MediaController extends FrameLayout {
super.setEnabled(enabled);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(MediaController.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(MediaController.class.getName());
+ }
+
private View.OnClickListener mRewListener = new View.OnClickListener() {
public void onClick(View v) {
int pos = mPlayer.getCurrentPosition();
diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java
index 134e4c4..0b30c84 100644
--- a/core/java/android/widget/MultiAutoCompleteTextView.java
+++ b/core/java/android/widget/MultiAutoCompleteTextView.java
@@ -23,7 +23,8 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.QwertyKeyListener;
import android.util.AttributeSet;
-import android.widget.MultiAutoCompleteTextView.Tokenizer;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
* An editable text view, extending {@link AutoCompleteTextView}, that
@@ -196,6 +197,18 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView {
editable.replace(start, end, mTokenizer.terminateToken(text));
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(MultiAutoCompleteTextView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(MultiAutoCompleteTextView.class.getName());
+ }
+
public static interface Tokenizer {
/**
* Returns the start of the token that ends at offset
diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java
index 13375bf..d395fb2 100644
--- a/core/java/android/widget/NumberPicker.java
+++ b/core/java/android/widget/NumberPicker.java
@@ -47,6 +47,7 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.InputMethodManager;
@@ -54,12 +55,12 @@ import com.android.internal.R;
/**
* A widget that enables the user to select a number form a predefined range.
- * The widget presents an input filed and up and down buttons for selecting the
+ * The widget presents an input field and up and down buttons for selecting the
* current value. Pressing/long pressing the up and down buttons increments and
- * decrements the current value respectively. Touching the input filed shows a
+ * decrements the current value respectively. Touching the input field shows a
* scroll wheel, tapping on which while shown and not moving allows direct edit
* of the current value. Sliding motions up or down hide the buttons and the
- * input filed, show the scroll wheel, and rotate the latter. Flinging is
+ * input field, show the scroll wheel, and rotate the latter. Flinging is
* also supported. The widget enables mapping from positions to strings such
* that instead the position index the corresponding string is displayed.
* <p>
@@ -70,6 +71,11 @@ import com.android.internal.R;
public class NumberPicker extends LinearLayout {
/**
+ * The number of items show in the selector wheel.
+ */
+ public static final int SELECTOR_WHEEL_ITEM_COUNT = 5;
+
+ /**
* The default update interval during long press.
*/
private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
@@ -583,10 +589,7 @@ public class NumberPicker extends LinearLayout {
OnClickListener onClickListener = new OnClickListener() {
public void onClick(View v) {
- InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
- if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
- inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
- }
+ hideSoftInput();
mInputText.clearFocus();
if (v.getId() == R.id.increment) {
changeCurrentByOne(true);
@@ -598,6 +601,7 @@ public class NumberPicker extends LinearLayout {
OnLongClickListener onLongClickListener = new OnLongClickListener() {
public boolean onLongClick(View v) {
+ hideSoftInput();
mInputText.clearFocus();
if (v.getId() == R.id.increment) {
postChangeCurrentByOneFromLongPress(true);
@@ -786,6 +790,7 @@ public class NumberPicker extends LinearLayout {
}
mBeginEditOnUpEvent = scrollersFinished;
mAdjustScrollerOnUpEvent = true;
+ hideSoftInput();
hideInputControls();
return true;
}
@@ -795,6 +800,7 @@ public class NumberPicker extends LinearLayout {
}
mAdjustScrollerOnUpEvent = false;
setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
+ hideSoftInput();
hideInputControls();
return true;
case MotionEvent.ACTION_MOVE:
@@ -804,6 +810,7 @@ public class NumberPicker extends LinearLayout {
mBeginEditOnUpEvent = false;
onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
setSelectorWheelState(SELECTOR_WHEEL_STATE_LARGE);
+ hideSoftInput();
hideInputControls();
return true;
}
@@ -1062,6 +1069,16 @@ public class NumberPicker extends LinearLayout {
}
/**
+ * Hides the soft input of it is active for the input text.
+ */
+ private void hideSoftInput() {
+ InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+ if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
+ inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ }
+
+ /**
* Computes the max width if no such specified as an attribute.
*/
private void tryComputeMaxWidth() {
@@ -1125,14 +1142,17 @@ public class NumberPicker extends LinearLayout {
* items shown on the selector wheel) the selector wheel wrapping is
* enabled.
* </p>
- *
+ * <p>
+ * <strong>Note:</strong> If the number of items, i.e. the range
+ * ({@link #getMaxValue()} - {@link #getMinValue()}) is less than
+ * {@link #SELECTOR_WHEEL_ITEM_COUNT}, the selector wheel will not
+ * wrap. Hence, in such a case calling this method is a NOP.
+ * </p>
* @param wrapSelectorWheel Whether to wrap.
*/
public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
- if (wrapSelectorWheel && (mMaxValue - mMinValue) < mSelectorIndices.length) {
- throw new IllegalStateException("Range less than selector items count.");
- }
- if (wrapSelectorWheel != mWrapSelectorWheel) {
+ final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
+ if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) {
mWrapSelectorWheel = wrapSelectorWheel;
updateIncrementAndDecrementButtonsVisibilityState();
}
@@ -1371,6 +1391,18 @@ public class NumberPicker extends LinearLayout {
// perceive this widget as several controls rather as a whole.
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(NumberPicker.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(NumberPicker.class.getName());
+ }
+
/**
* Makes a measure spec that tries greedily to use the max value.
*
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
index df88fec..ace3f60 100644
--- a/core/java/android/widget/ProgressBar.java
+++ b/core/java/android/widget/ProgressBar.java
@@ -45,6 +45,7 @@ import android.view.View;
import android.view.ViewDebug;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
@@ -1124,10 +1125,17 @@ public class ProgressBar extends View {
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ProgressBar.class.getName());
event.setItemCount(mMax);
event.setCurrentItemIndex(mProgress);
}
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ProgressBar.class.getName());
+ }
+
/**
* Schedule a command for sending an accessibility event.
* </br>
diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java
index adc0fb0..786afe2 100644
--- a/core/java/android/widget/QuickContactBadge.java
+++ b/core/java/android/widget/QuickContactBadge.java
@@ -36,6 +36,8 @@ import android.provider.ContactsContract.RawContacts;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
* Widget used to show an image with the standard QuickContact badge
@@ -228,6 +230,18 @@ public class QuickContactBadge extends ImageView implements OnClickListener {
}
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(QuickContactBadge.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(QuickContactBadge.class.getName());
+ }
+
/**
* 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}
diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java
index 9fa649f..b6dac3e 100644
--- a/core/java/android/widget/RadioButton.java
+++ b/core/java/android/widget/RadioButton.java
@@ -19,6 +19,7 @@ package android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.R;
@@ -85,4 +86,16 @@ public class RadioButton extends CompoundButton {
event.getText().add(mContext.getString(R.string.radiobutton_not_selected));
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(RadioButton.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(RadioButton.class.getName());
+ }
}
diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java
index 393346a..7f53ffd 100644
--- a/core/java/android/widget/RadioGroup.java
+++ b/core/java/android/widget/RadioGroup.java
@@ -23,6 +23,8 @@ import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
@@ -236,6 +238,18 @@ public class RadioGroup extends LinearLayout {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(RadioGroup.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(RadioGroup.class.getName());
+ }
+
/**
* <p>This set of layout parameters defaults the width and the height of
* the children to {@link #WRAP_CONTENT} when they are not specified in the
diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java
index 9e6ff4b..e69577b 100644
--- a/core/java/android/widget/RatingBar.java
+++ b/core/java/android/widget/RatingBar.java
@@ -21,6 +21,8 @@ import android.content.res.TypedArray;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.Shape;
import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.R;
@@ -324,4 +326,15 @@ public class RatingBar extends AbsSeekBar {
super.setMax(max);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(RatingBar.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(RatingBar.class.getName());
+ }
}
diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java
index 12a93ac..e4b8f34 100644
--- a/core/java/android/widget/RelativeLayout.java
+++ b/core/java/android/widget/RelativeLayout.java
@@ -18,10 +18,10 @@ package android.widget;
import com.android.internal.R;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.HashSet;
-import java.util.LinkedList;
+import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -40,6 +40,7 @@ import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
import static android.util.Log.d;
@@ -151,6 +152,15 @@ public class RelativeLayout extends ViewGroup {
private static final int VERB_COUNT = 16;
+
+ private static final int[] RULES_VERTICAL = {
+ ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM
+ };
+
+ private static final int[] RULES_HORIZONTAL = {
+ LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT
+ };
+
private View mBaselineView = null;
private boolean mHasBaselineAlignedChild;
@@ -284,14 +294,13 @@ public class RelativeLayout extends ViewGroup {
if (DEBUG_GRAPH) {
d(LOG_TAG, "=== Sorted vertical children");
- graph.log(getResources(), ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM);
+ graph.log(getResources(), RULES_VERTICAL);
d(LOG_TAG, "=== Sorted horizontal children");
- graph.log(getResources(), LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT);
+ graph.log(getResources(), RULES_HORIZONTAL);
}
- graph.getSortedViews(mSortedVerticalChildren, ABOVE, BELOW, ALIGN_BASELINE,
- ALIGN_TOP, ALIGN_BOTTOM);
- graph.getSortedViews(mSortedHorizontalChildren, LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT);
+ graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
+ graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
if (DEBUG_GRAPH) {
d(LOG_TAG, "=== Ordered list of vertical children");
@@ -977,6 +986,18 @@ public class RelativeLayout extends ViewGroup {
return false;
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(RelativeLayout.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(RelativeLayout.class.getName());
+ }
+
/**
* Compares two views in left-to-right and top-to-bottom fashion.
*/
@@ -1216,7 +1237,7 @@ public class RelativeLayout extends ViewGroup {
* Temporary data structure used to build the list of roots
* for this graph.
*/
- private LinkedList<Node> mRoots = new LinkedList<Node>();
+ private ArrayDeque<Node> mRoots = new ArrayDeque<Node>();
/**
* Clears the graph.
@@ -1261,18 +1282,18 @@ public class RelativeLayout extends ViewGroup {
* @param rules The list of rules to take into account.
*/
void getSortedViews(View[] sorted, int... rules) {
- final LinkedList<Node> roots = findRoots(rules);
+ final ArrayDeque<Node> roots = findRoots(rules);
int index = 0;
- while (roots.size() > 0) {
- final Node node = roots.removeFirst();
+ Node node;
+ while ((node = roots.pollLast()) != null) {
final View view = node.view;
final int key = view.getId();
sorted[index++] = view;
- final HashSet<Node> dependents = node.dependents;
- for (Node dependent : dependents) {
+ final HashMap<Node, DependencyGraph> dependents = node.dependents;
+ for (Node dependent : dependents.keySet()) {
final SparseArray<Node> dependencies = dependent.dependencies;
dependencies.remove(key);
@@ -1297,7 +1318,7 @@ public class RelativeLayout extends ViewGroup {
*
* @return A list of node, each being a root of the graph
*/
- private LinkedList<Node> findRoots(int[] rulesFilter) {
+ private ArrayDeque<Node> findRoots(int[] rulesFilter) {
final SparseArray<Node> keyNodes = mKeyNodes;
final ArrayList<Node> nodes = mNodes;
final int count = nodes.size();
@@ -1330,20 +1351,20 @@ public class RelativeLayout extends ViewGroup {
continue;
}
// Add the current node as a dependent
- dependency.dependents.add(node);
+ dependency.dependents.put(node, this);
// Add a dependency to the current node
node.dependencies.put(rule, dependency);
}
}
}
- final LinkedList<Node> roots = mRoots;
+ final ArrayDeque<Node> roots = mRoots;
roots.clear();
// Finds all the roots in the graph: all nodes with no dependencies
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
- if (node.dependencies.size() == 0) roots.add(node);
+ if (node.dependencies.size() == 0) roots.addLast(node);
}
return roots;
@@ -1356,7 +1377,7 @@ public class RelativeLayout extends ViewGroup {
* @param rules The list of rules to take into account.
*/
void log(Resources resources, int... rules) {
- final LinkedList<Node> roots = findRoots(rules);
+ final ArrayDeque<Node> roots = findRoots(rules);
for (Node node : roots) {
printNode(resources, node);
}
@@ -1382,7 +1403,7 @@ public class RelativeLayout extends ViewGroup {
if (node.dependents.size() == 0) {
printViewId(resources, node.view);
} else {
- for (Node dependent : node.dependents) {
+ for (Node dependent : node.dependents.keySet()) {
StringBuilder buffer = new StringBuilder();
appendViewId(resources, node, buffer);
printdependents(resources, dependent, buffer);
@@ -1397,7 +1418,7 @@ public class RelativeLayout extends ViewGroup {
if (node.dependents.size() == 0) {
d(LOG_TAG, buffer.toString());
} else {
- for (Node dependent : node.dependents) {
+ for (Node dependent : node.dependents.keySet()) {
StringBuilder subBuffer = new StringBuilder(buffer);
printdependents(resources, dependent, subBuffer);
}
@@ -1420,7 +1441,7 @@ public class RelativeLayout extends ViewGroup {
* The list of dependents for this node; a dependent is a node
* that needs this node to be processed first.
*/
- final HashSet<Node> dependents = new HashSet<Node>();
+ final HashMap<Node, DependencyGraph> dependents = new HashMap<Node, DependencyGraph>();
/**
* The list of dependencies for this node.
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 7b43032..586fdf4 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -68,6 +68,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
private RemoteViewsAdapterServiceConnection mServiceConnection;
private WeakReference<RemoteAdapterConnectionCallback> mCallback;
private FixedSizeRemoteViewsCache mCache;
+ private int mVisibleWindowLowerBound;
+ private int mVisibleWindowUpperBound;
// A flag to determine whether we should notify data set changed after we connect
private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;
@@ -765,7 +767,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
if (position > -1) {
// Load the item, and notify any existing RemoteViewsFrameLayouts
- updateRemoteViews(position, isRequested);
+ updateRemoteViews(position, isRequested, true);
// Queue up for the next one to load
loadNextIndexInBackground();
@@ -827,8 +829,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
}
- private void updateRemoteViews(final int position, boolean isRequested) {
- if (!mServiceConnection.isConnected()) return;
+ private void updateRemoteViews(final int position, boolean isRequested, boolean
+ notifyWhenLoaded) {
IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
// Load the item information from the remote service
@@ -864,12 +866,14 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
// there is new data for it.
final RemoteViews rv = remoteViews;
final int typeId = mCache.getMetaDataAt(position).typeId;
- mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId);
- }
- });
+ if (notifyWhenLoaded) {
+ mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId);
+ }
+ });
+ }
}
}
@@ -929,6 +933,16 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
return typeId;
}
+ /**
+ * This method allows an AdapterView using this Adapter to provide information about which
+ * views are currently being displayed. This allows for certain optimizations and preloading
+ * which wouldn't otherwise be possible.
+ */
+ public void setVisibleRangeHint(int lowerBound, int upperBound) {
+ mVisibleWindowLowerBound = lowerBound;
+ mVisibleWindowUpperBound = upperBound;
+ }
+
public View getView(int position, View convertView, ViewGroup parent) {
// "Request" an index so that we can queue it for loading, initiate subsequent
// preloading, etc.
@@ -1059,6 +1073,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
// Re-request the new metadata (only after the notification to the factory)
updateTemporaryMetaData();
+ // Pre-load (our best guess of) the views which are currently visible in the AdapterView.
+ // This mitigates flashing and flickering of loading views when a widget notifies that
+ // its data has changed.
+ for (int i = mVisibleWindowLowerBound; i <= mVisibleWindowUpperBound; i++) {
+ updateRemoteViews(i, false, false);
+ }
+
// Propagate the notification back to the base adapter
mMainQueue.post(new Runnable() {
@Override
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index 767eaee..3ffc0fe 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -721,12 +721,14 @@ public class ScrollView extends FrameLayout {
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ScrollView.class.getName());
info.setScrollable(getScrollRange() > 0);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ScrollView.class.getName());
final boolean scrollable = getScrollRange() > 0;
event.setScrollable(scrollable);
event.setScrollX(mScrollX);
diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java
index 9d2ff2e..99cd0b8 100644
--- a/core/java/android/widget/SearchView.java
+++ b/core/java/android/widget/SearchView.java
@@ -35,7 +35,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
import android.speech.RecognizerIntent;
import android.text.Editable;
import android.text.InputType;
@@ -51,6 +50,8 @@ import android.view.CollapsibleActionView;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView.OnItemClickListener;
@@ -1206,6 +1207,18 @@ public class SearchView extends LinearLayout implements CollapsibleActionView {
setIconified(false);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(SearchView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(SearchView.class.getName());
+ }
+
private void adjustDropDownSizeAndPosition() {
if (mDropDownAnchor.getWidth() > 1) {
Resources res = getContext().getResources();
diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java
index c76728f..2737f94 100644
--- a/core/java/android/widget/SeekBar.java
+++ b/core/java/android/widget/SeekBar.java
@@ -18,6 +18,8 @@ package android.widget;
import android.content.Context;
import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
@@ -117,5 +119,16 @@ public class SeekBar extends AbsSeekBar {
mOnSeekBarChangeListener.onStopTrackingTouch(this);
}
}
-
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(SeekBar.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(SeekBar.class.getName());
+ }
}
diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java
index bb27b73..22e9ef1 100644
--- a/core/java/android/widget/ShareActionProvider.java
+++ b/core/java/android/widget/ShareActionProvider.java
@@ -279,6 +279,7 @@ public class ShareActionProvider extends ActionProvider {
final int itemId = item.getItemId();
Intent launchIntent = dataModel.chooseActivity(itemId);
if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(launchIntent);
}
return true;
diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java
index bdeb5c2..14edd10 100644
--- a/core/java/android/widget/SlidingDrawer.java
+++ b/core/java/android/widget/SlidingDrawer.java
@@ -32,6 +32,7 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
* SlidingDrawer hides content out of the screen and allows the user to drag a handle
@@ -810,6 +811,18 @@ public class SlidingDrawer extends ViewGroup {
}
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(SlidingDrawer.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(SlidingDrawer.class.getName());
+ }
+
private void closeDrawer() {
moveHandle(COLLAPSED_FULL_CLOSED);
mContent.setVisibility(View.GONE);
diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java
index da3134a..570f0f9 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -102,7 +102,8 @@ public class SpellChecker implements SpellCheckerSessionListener {
mTextServicesManager = (TextServicesManager) mTextView.getContext().
getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
- if (!mTextServicesManager.isSpellCheckerEnabled()) {
+ if (!mTextServicesManager.isSpellCheckerEnabled()
+ || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
mSpellCheckerSession = null;
} else {
mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
@@ -120,9 +121,6 @@ public class SpellChecker implements SpellCheckerSessionListener {
// Remove existing misspelled SuggestionSpans
mTextView.removeMisspelledSpans((Editable) mTextView.getText());
-
- // This class is the listener for locale change: warn other locale-aware objects
- mTextView.onLocaleChanged();
}
private void setLocale(Locale locale) {
@@ -152,7 +150,7 @@ public class SpellChecker implements SpellCheckerSessionListener {
final int length = mSpellParsers.length;
for (int i = 0; i < length; i++) {
- mSpellParsers[i].finish();
+ mSpellParsers[i].stop();
}
if (mSpellRunnable != null) {
@@ -217,6 +215,7 @@ public class SpellChecker implements SpellCheckerSessionListener {
if (!isSessionActive()) return;
+ // Find first available SpellParser from pool
final int length = mSpellParsers.length;
for (int i = 0; i < length; i++) {
final SpellParser spellParser = mSpellParsers[i];
@@ -278,6 +277,12 @@ public class SpellChecker implements SpellCheckerSessionListener {
}
@Override
+ public void onGetSuggestionsForSentence(SuggestionsInfo[] results) {
+ // TODO: Handle the position and length for each suggestion
+ onGetSuggestions(results);
+ }
+
+ @Override
public void onGetSuggestions(SuggestionsInfo[] results) {
Editable editable = (Editable) mTextView.getText();
@@ -337,56 +342,15 @@ public class SpellChecker implements SpellCheckerSessionListener {
final int end = editable.getSpanEnd(spellCheckSpan);
if (start < 0 || end <= start) return; // span was removed in the meantime
- // Other suggestion spans may exist on that region, with identical suggestions, filter
- // them out to avoid duplicates.
- SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class);
- final int length = suggestionSpans.length;
- for (int i = 0; i < length; i++) {
- final int spanStart = editable.getSpanStart(suggestionSpans[i]);
- final int spanEnd = editable.getSpanEnd(suggestionSpans[i]);
- if (spanStart != start || spanEnd != end) {
- // Nulled (to avoid new array allocation) if not on that exact same region
- suggestionSpans[i] = null;
- }
- }
-
final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
- String[] suggestions;
if (suggestionsCount <= 0) {
// A negative suggestion count is possible
- suggestions = ArrayUtils.emptyArray(String.class);
- } else {
- int numberOfSuggestions = 0;
- suggestions = new String[suggestionsCount];
-
- for (int i = 0; i < suggestionsCount; i++) {
- final String spellSuggestion = suggestionsInfo.getSuggestionAt(i);
- if (spellSuggestion == null) break;
- boolean suggestionFound = false;
-
- for (int j = 0; j < length && !suggestionFound; j++) {
- if (suggestionSpans[j] == null) break;
-
- String[] suggests = suggestionSpans[j].getSuggestions();
- for (int k = 0; k < suggests.length; k++) {
- if (spellSuggestion.equals(suggests[k])) {
- // The suggestion is already provided by an other SuggestionSpan
- suggestionFound = true;
- break;
- }
- }
- }
-
- if (!suggestionFound) {
- suggestions[numberOfSuggestions++] = spellSuggestion;
- }
- }
+ return;
+ }
- if (numberOfSuggestions != suggestionsCount) {
- String[] newSuggestions = new String[numberOfSuggestions];
- System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions);
- suggestions = newSuggestions;
- }
+ String[] suggestions = new String[suggestionsCount];
+ for (int i = 0; i < suggestionsCount; i++) {
+ suggestions[i] = suggestionsInfo.getSuggestionAt(i);
}
SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
@@ -400,18 +364,25 @@ public class SpellChecker implements SpellCheckerSessionListener {
private Object mRange = new Object();
public void init(int start, int end) {
- ((Editable) mTextView.getText()).setSpan(mRange, start, end,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- public void finish() {
- ((Editable) mTextView.getText()).removeSpan(mRange);
+ setRangeSpan((Editable) mTextView.getText(), start, end);
}
public boolean isFinished() {
return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
}
+ public void stop() {
+ removeRangeSpan((Editable) mTextView.getText());
+ }
+
+ private void setRangeSpan(Editable editable, int start, int end) {
+ editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private void removeRangeSpan(Editable editable) {
+ editable.removeSpan(mRange);
+ }
+
public void parse() {
Editable editable = (Editable) mTextView.getText();
// Iterate over the newly added text and schedule new SpellCheckSpans
@@ -433,7 +404,7 @@ public class SpellChecker implements SpellCheckerSessionListener {
wordEnd = mWordIterator.getEnd(wordStart);
}
if (wordEnd == BreakIterator.DONE) {
- editable.removeSpan(mRange);
+ removeRangeSpan(editable);
return;
}
@@ -511,9 +482,10 @@ public class SpellChecker implements SpellCheckerSessionListener {
}
if (scheduleOtherSpellCheck) {
- editable.setSpan(mRange, wordStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ // Update range span: start new spell check from last wordStart
+ setRangeSpan(editable, wordStart, end);
} else {
- editable.removeSpan(mRange);
+ removeRangeSpan(editable);
}
spellCheck();
diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java
index ec3790e..ecf19b3 100644
--- a/core/java/android/widget/Spinner.java
+++ b/core/java/android/widget/Spinner.java
@@ -29,6 +29,8 @@ import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
@@ -456,12 +458,24 @@ public class Spinner extends AbsSpinner implements OnClickListener {
return handled;
}
-
+
public void onClick(DialogInterface dialog, int which) {
setSelection(which);
dialog.dismiss();
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Spinner.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Spinner.class.getName());
+ }
+
/**
* Sets the prompt to display when the dialog is shown.
* @param prompt the prompt to set
diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java
index 03e6e99..22df3bc 100644
--- a/core/java/android/widget/StackView.java
+++ b/core/java/android/widget/StackView.java
@@ -40,6 +40,8 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.LinearInterpolator;
import android.widget.RemoteViews.RemoteView;
@@ -1216,6 +1218,18 @@ public class StackView extends AdapterViewAnimator {
measureChildren();
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(StackView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(StackView.class.getName());
+ }
+
class LayoutParams extends ViewGroup.LayoutParams {
int horizontalOffset;
int verticalOffset;
diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java
index 02c9d03..334b9c4 100644
--- a/core/java/android/widget/Switch.java
+++ b/core/java/android/widget/Switch.java
@@ -35,6 +35,7 @@ import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.R;
@@ -651,4 +652,16 @@ public class Switch extends CompoundButton {
mThumbDrawable.jumpToCurrentState();
mTrackDrawable.jumpToCurrentState();
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Switch.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Switch.class.getName());
+ }
}
diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java
index 88d7230..9b292be 100644
--- a/core/java/android/widget/TabHost.java
+++ b/core/java/android/widget/TabHost.java
@@ -33,6 +33,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import java.util.ArrayList;
import java.util.List;
@@ -321,6 +323,18 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1");
}
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TabHost.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TabHost.class.getName());
+ }
+
public void setCurrentTab(int index) {
if (index < 0 || index >= mTabSpecs.size()) {
return;
diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java
index 80bfe99..8901037 100644
--- a/core/java/android/widget/TabWidget.java
+++ b/core/java/android/widget/TabWidget.java
@@ -29,6 +29,7 @@ import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
*
@@ -416,10 +417,28 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener {
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TabWidget.class.getName());
event.setItemCount(getTabCount());
event.setCurrentItemIndex(mSelectedTab);
}
+
+ @Override
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+ // this class fires events only when tabs are focused or selected
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) {
+ event.recycle();
+ return;
+ }
+ super.sendAccessibilityEventUnchecked(event);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TabWidget.class.getName());
+ }
+
/**
* Sets the current tab and focuses the UI on it.
* This method makes sure that the focused tab matches the selected
@@ -485,16 +504,6 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener {
mSelectedTab = -1;
}
- @Override
- public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
- // this class fires events only when tabs are focused or selected
- if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) {
- event.recycle();
- return;
- }
- super.sendAccessibilityEventUnchecked(event);
- }
-
/**
* Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
*/
diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java
index 842b087..f5d3746 100644
--- a/core/java/android/widget/TableLayout.java
+++ b/core/java/android/widget/TableLayout.java
@@ -24,6 +24,8 @@ import android.util.AttributeSet;
import android.util.SparseBooleanArray;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import java.util.regex.Pattern;
@@ -658,6 +660,18 @@ public class TableLayout extends LinearLayout {
return new LayoutParams(p);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TableLayout.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TableLayout.class.getName());
+ }
+
/**
* <p>This set of layout parameters enforces the width of each child to be
* {@link #MATCH_PARENT} and the height of each child to be
diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java
index 3fd4631..01c4c2c 100644
--- a/core/java/android/widget/TableRow.java
+++ b/core/java/android/widget/TableRow.java
@@ -24,6 +24,8 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
@@ -377,6 +379,18 @@ public class TableRow extends LinearLayout {
return new LayoutParams(p);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TableRow.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TableRow.class.getName());
+ }
+
/**
* <p>Set of layout parameters used in table rows.</p>
*
diff --git a/core/java/android/widget/TextSwitcher.java b/core/java/android/widget/TextSwitcher.java
index a8794a3..1aefd2b 100644
--- a/core/java/android/widget/TextSwitcher.java
+++ b/core/java/android/widget/TextSwitcher.java
@@ -21,6 +21,8 @@ import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
* Specialized {@link android.widget.ViewSwitcher} that contains
@@ -88,4 +90,16 @@ public class TextSwitcher extends ViewSwitcher {
public void setCurrentText(CharSequence text) {
((TextView)getCurrentView()).setText(text);
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TextSwitcher.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TextSwitcher.class.getName());
+ }
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index b9d3d43..3ce0a3e 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -84,6 +84,7 @@ import android.text.method.TimeKeyListener;
import android.text.method.TransformationMethod;
import android.text.method.TransformationMethod2;
import android.text.method.WordIterator;
+import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.EasyEditSpan;
import android.text.style.ParagraphStyle;
@@ -101,9 +102,11 @@ import android.util.Log;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.ActionMode.Callback;
+import android.view.DisplayList;
import android.view.DragEvent;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
+import android.view.HardwareCanvas;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -253,10 +256,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private float mShadowRadius, mShadowDx, mShadowDy;
- private static final int PREDRAW_NOT_REGISTERED = 0;
- private static final int PREDRAW_PENDING = 1;
- private static final int PREDRAW_DONE = 2;
- private int mPreDrawState = PREDRAW_NOT_REGISTERED;
+ private boolean mPreDrawRegistered;
private TextUtils.TruncateAt mEllipsize = null;
@@ -283,6 +283,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private Drawables mDrawables;
+ private DisplayList mTextDisplayList;
+ private boolean mTextDisplayListIsValid;
+
private CharSequence mError;
private boolean mErrorWasChanged;
private ErrorPopup mPopup;
@@ -336,6 +339,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private int mTextEditSuggestionItemLayout;
private SuggestionsPopupWindow mSuggestionsPopupWindow;
private SuggestionRangeSpan mSuggestionRangeSpan;
+ private Runnable mShowSuggestionRunnable;
private int mCursorDrawableRes;
private final Drawable[] mCursorDrawable = new Drawable[2];
@@ -351,7 +355,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private float mLastDownPositionX, mLastDownPositionY;
private Callback mCustomSelectionActionModeCallback;
- private final int mSquaredTouchSlopDistance;
// Set when this TextView gained focus with some text selected. Will start selection mode.
private boolean mCreatedWithASelection = false;
@@ -437,15 +440,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
this(context, null);
}
- public TextView(Context context,
- AttributeSet attrs) {
+ public TextView(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
@SuppressWarnings("deprecation")
- public TextView(Context context,
- AttributeSet attrs,
- int defStyle) {
+ public TextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mText = "";
@@ -1128,10 +1128,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setLongClickable(longClickable);
prepareCursorControllers();
-
- final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
- final int touchSlop = viewConfiguration.getScaledTouchSlop();
- mSquaredTouchSlopDistance = touchSlop * touchSlop;
}
private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
@@ -1202,14 +1198,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
imm.hideSoftInputFromWindow(getWindowToken(), 0);
}
}
+
super.setEnabled(enabled);
- prepareCursorControllers();
+
if (enabled) {
// Make sure IME is updated with current editor info.
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
}
+ prepareCursorControllers();
+
// start or stop the cursor blinking as appropriate
makeBlink();
}
@@ -3193,8 +3192,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int n = mFilters.length;
for (int i = 0; i < n; i++) {
- CharSequence out = mFilters[i].filter(text, 0, text.length(),
- EMPTY_SPANNED, 0, 0);
+ CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
if (out != null) {
text = out;
}
@@ -4386,26 +4384,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private void registerForPreDraw() {
- final ViewTreeObserver observer = getViewTreeObserver();
-
- if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
- observer.addOnPreDrawListener(this);
- mPreDrawState = PREDRAW_PENDING;
- } else if (mPreDrawState == PREDRAW_DONE) {
- mPreDrawState = PREDRAW_PENDING;
+ if (!mPreDrawRegistered) {
+ getViewTreeObserver().addOnPreDrawListener(this);
+ mPreDrawRegistered = true;
}
-
- // else state is PREDRAW_PENDING, so keep waiting.
}
/**
* {@inheritDoc}
*/
public boolean onPreDraw() {
- if (mPreDrawState != PREDRAW_PENDING) {
- return true;
- }
-
if (mLayout == null) {
assumeLayout();
}
@@ -4456,7 +4444,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
startSelectionActionMode();
}
- mPreDrawState = PREDRAW_DONE;
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ mPreDrawRegistered = false;
+
return !changed;
}
@@ -4491,10 +4481,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- final ViewTreeObserver observer = getViewTreeObserver();
- if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
- observer.removeOnPreDrawListener(this);
- mPreDrawState = PREDRAW_NOT_REGISTERED;
+ if (mPreDrawRegistered) {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ mPreDrawRegistered = false;
}
if (mError != null) {
@@ -4513,10 +4502,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mSelectionModifierCursorController.onDetached();
}
+ if (mShowSuggestionRunnable != null) {
+ removeCallbacks(mShowSuggestionRunnable);
+ }
+
hideControllers();
resetResolvedDrawables();
+ if (mTextDisplayList != null) {
+ mTextDisplayList.invalidate();
+ }
+
if (mSpellChecker != null) {
mSpellChecker.closeSession();
// Forces the creation of a new SpellChecker next time this window is created.
@@ -4759,12 +4756,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@Override
protected void onDraw(Canvas canvas) {
- if (mPreDrawState == PREDRAW_DONE) {
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.removeOnPreDrawListener(this);
- mPreDrawState = PREDRAW_NOT_REGISTERED;
- }
-
if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
restartMarqueeIfNeeded();
@@ -4967,17 +4958,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- /* Comment out until we decide what to do about animations
- boolean isLinearTextOn = false;
- if (currentTransformation != null) {
- isLinearTextOn = mTextPaint.isLinearTextOn();
- Matrix m = currentTransformation.getMatrix();
- if (!m.isIdentity()) {
- // mTextPaint.setLinearTextOn(true);
- }
- }
- */
-
final InputMethodState ims = mInputMethodState;
final int cursorOffsetVertical = voffsetCursor - voffsetText;
if (ims != null && ims.mBatchEditNesting == 0) {
@@ -5035,18 +5015,38 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
highlight = null;
}
- layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+ if (canHaveDisplayList() && canvas.isHardwareAccelerated()) {
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
- if (mMarquee != null && mMarquee.shouldDrawGhost()) {
- canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
+ if (mTextDisplayList == null || !mTextDisplayList.isValid() ||
+ !mTextDisplayListIsValid) {
+ if (mTextDisplayList == null) {
+ mTextDisplayList = getHardwareRenderer().createDisplayList();
+ }
+
+ final HardwareCanvas hardwareCanvas = mTextDisplayList.start();
+ try {
+ hardwareCanvas.setViewport(width, height);
+ // The dirty rect should always be null for a display list
+ hardwareCanvas.onPreDraw(null);
+ layout.draw(hardwareCanvas, highlight, mHighlightPaint, cursorOffsetVertical);
+ } finally {
+ hardwareCanvas.onPostDraw();
+ mTextDisplayList.end();
+ mTextDisplayListIsValid = true;
+ }
+ }
+ ((HardwareCanvas) canvas).drawDisplayList(mTextDisplayList,
+ mScrollX + width, mScrollY + height, null);
+ } else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
- /* Comment out until we decide what to do about animations
- if (currentTransformation != null) {
- mTextPaint.setLinearTextOn(isLinearTextOn);
+ if (mMarquee != null && mMarquee.shouldDrawGhost()) {
+ canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
+ layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
- */
canvas.restore();
}
@@ -5251,10 +5251,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
state.handleUpEvent(event);
}
if (event.isTracking() && !event.isCanceled()) {
- if (isInSelectionMode) {
- stopSelectionActionMode();
- return true;
- }
+ stopSelectionActionMode();
+ return true;
}
}
}
@@ -5599,11 +5597,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return super.onKeyUp(keyCode, event);
}
- @Override public boolean onCheckIsTextEditor() {
+ @Override
+ public boolean onCheckIsTextEditor() {
return mInputType != EditorInfo.TYPE_NULL;
}
- @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
if (mInputMethodState == null) {
mInputMethodState = new InputMethodState();
@@ -6083,6 +6083,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
+ mBoring = mHintBoring = null;
+
// Since it depends on the value of mLayout
prepareCursorControllers();
}
@@ -6775,6 +6777,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed) mTextDisplayListIsValid = false;
+ }
+
/**
* Returns true if anything changed.
*/
@@ -7557,6 +7565,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
protected void onSelectionChanged(int selStart, int selEnd) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
+ mTextDisplayListIsValid = false;
}
/**
@@ -7636,6 +7645,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
updateSpellCheckSpans(start, start + after, false);
+ mTextDisplayListIsValid = false;
// Hide the controllers as soon as text is modified (typing, procedural...)
// We do not hide the span controllers, since they can be added when a new text is
@@ -7738,7 +7748,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- if (what instanceof UpdateAppearance || what instanceof ParagraphStyle) {
+ if (what instanceof UpdateAppearance || what instanceof ParagraphStyle ||
+ what instanceof CharacterStyle) {
if (ims == null || ims.mBatchEditNesting == 0) {
invalidate();
mHighlightPathBogus = true;
@@ -7746,6 +7757,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else {
ims.mContentChanged = true;
}
+ mTextDisplayListIsValid = false;
}
if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
@@ -8311,6 +8323,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
getSelectionController().onTouchEvent(event);
}
+ if (mShowSuggestionRunnable != null) {
+ removeCallbacks(mShowSuggestionRunnable);
+ }
+
if (action == MotionEvent.ACTION_DOWN) {
mLastDownPositionX = event.getX();
mLastDownPositionY = event.getY();
@@ -8334,7 +8350,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
- !shouldIgnoreActionUpEvent() && isFocused();
+ !mIgnoreActionUpEvent && isFocused();
if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
&& mText instanceof Spannable && mLayout != null) {
@@ -8351,7 +8367,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
getSelectionEnd(), ClickableSpan.class);
- if (links.length != 0) {
+ if (links.length > 0) {
links[0].onClick(this);
handled = true;
}
@@ -8368,13 +8384,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
hideControllers();
if (!selectAllGotFocus && mText.length() > 0) {
+ // Move cursor
+ final int offset = getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection((Spannable) mText, offset);
if (mSpellChecker != null) {
// When the cursor moves, the word that was typed may need spell check
mSpellChecker.onSelectionChanged();
}
if (!extractedTextModeWillBeStarted()) {
if (isCursorInsideEasyCorrectionSpan()) {
- showSuggestions();
+ if (mShowSuggestionRunnable == null) {
+ mShowSuggestionRunnable = new Runnable() {
+ public void run() {
+ showSuggestions();
+ }
+ };
+ }
+ postDelayed(mShowSuggestionRunnable,
+ ViewConfiguration.getDoubleTapTimeout());
} else if (hasInsertionController()) {
getInsertionController().show();
}
@@ -8510,17 +8537,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mIgnoreActionUpEvent = true;
}
- /**
- * This method is only valid during a touch event.
- *
- * @return true when the ACTION_UP event should be ignored, false otherwise.
- *
- * @hide
- */
- public boolean shouldIgnoreActionUpEvent() {
- return mIgnoreActionUpEvent;
- }
-
@Override
public boolean onTrackballEvent(MotionEvent event) {
if (mMovement != null && mText instanceof Spannable &&
@@ -8579,7 +8595,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @return True when the TextView isFocused and has a valid zero-length selection (cursor).
*/
private boolean shouldBlink() {
- if (!isFocused()) return false;
+ if (!isCursorVisible() || !isFocused()) return false;
final int start = getSelectionStart();
if (start < 0) return false;
@@ -8591,13 +8607,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private void makeBlink() {
- if (isCursorVisible()) {
- if (shouldBlink()) {
- mShowCursor = SystemClock.uptimeMillis();
- if (mBlink == null) mBlink = new Blink(this);
- mBlink.removeCallbacks(mBlink);
- mBlink.postAtTime(mBlink, mShowCursor + BLINK);
- }
+ if (shouldBlink()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ if (mBlink == null) mBlink = new Blink(this);
+ mBlink.removeCallbacks(mBlink);
+ mBlink.postAtTime(mBlink, mShowCursor + BLINK);
} else {
if (mBlink != null) mBlink.removeCallbacks(mBlink);
}
@@ -8903,14 +8917,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
wordIterator.setCharSequence(mText, minOffset, maxOffset);
selectionStart = wordIterator.getBeginning(minOffset);
- if (selectionStart == BreakIterator.DONE) return false;
-
selectionEnd = wordIterator.getEnd(maxOffset);
- if (selectionEnd == BreakIterator.DONE) return false;
- if (selectionStart == selectionEnd) {
+ if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
+ selectionStart == selectionEnd) {
// Possible when the word iterator does not properly handle the text's language
- long range = getCharRange(selectionStart);
+ long range = getCharRange(minOffset);
selectionStart = extractRangeStartFromLong(range);
selectionEnd = extractRangeEndFromLong(range);
}
@@ -9003,6 +9015,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TextView.class.getName());
final boolean isPassword = hasPasswordTransformationMethod();
event.setPassword(isPassword);
@@ -9017,11 +9030,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TextView.class.getName());
final boolean isPassword = hasPasswordTransformationMethod();
+ info.setPassword(isPassword);
+
if (!isPassword) {
info.setText(getTextForAccessibility());
}
- info.setPassword(isPassword);
}
@Override
@@ -9219,7 +9234,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
boolean vibrate = true;
if (super.performLongClick()) {
- mDiscardNextActionUp = true;
handled = true;
}
@@ -9722,11 +9736,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void show() {
if (!(mText instanceof Editable)) return;
- updateSuggestions();
- mCursorWasVisibleBeforeSuggestions = mCursorVisible;
- setCursorVisible(false);
- mIsShowingUp = true;
- super.show();
+ if (updateSuggestions()) {
+ mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+ setCursorVisible(false);
+ mIsShowingUp = true;
+ super.show();
+ }
}
@Override
@@ -9782,11 +9797,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
super.hide();
}
- private void updateSuggestions() {
+ private boolean updateSuggestions() {
Spannable spannable = (Spannable) TextView.this.mText;
SuggestionSpan[] suggestionSpans = getSuggestionSpans();
final int nbSpans = suggestionSpans.length;
+ // Suggestions are shown after a delay: the underlying spans may have been removed
+ if (nbSpans == 0) return false;
mNumberOfSuggestions = 0;
int spanUnionStart = mText.length();
@@ -9812,17 +9829,34 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
String[] suggestions = suggestionSpan.getSuggestions();
int nbSuggestions = suggestions.length;
for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
- SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
- suggestionInfo.suggestionSpan = suggestionSpan;
- suggestionInfo.suggestionIndex = suggestionIndex;
- suggestionInfo.text.replace(0, suggestionInfo.text.length(),
- suggestions[suggestionIndex]);
+ String suggestion = suggestions[suggestionIndex];
+
+ boolean suggestionIsDuplicate = false;
+ for (int i = 0; i < mNumberOfSuggestions; i++) {
+ if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
+ SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
+ final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
+ final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
+ if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
+ suggestionIsDuplicate = true;
+ break;
+ }
+ }
+ }
- mNumberOfSuggestions++;
- if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
- // Also end outer for loop
- spanIndex = nbSpans;
- break;
+ if (!suggestionIsDuplicate) {
+ SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+ suggestionInfo.suggestionSpan = suggestionSpan;
+ suggestionInfo.suggestionIndex = suggestionIndex;
+ suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
+
+ mNumberOfSuggestions++;
+
+ if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
+ // Also end outer for loop
+ spanIndex = nbSpans;
+ break;
+ }
}
}
}
@@ -9831,7 +9865,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
}
- // Add to dictionary item if there is a span with the misspelled flag
+ // Add "Add to dictionary" item if there is a span with the misspelled flag
if (misspelledSpan != null) {
final int misspelledStart = spannable.getSpanStart(misspelledSpan);
final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
@@ -9872,6 +9906,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mSuggestionsAdapter.notifyDataSetChanged();
+ return true;
}
private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
@@ -9889,8 +9924,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// Add the text before and after the span.
- suggestionInfo.text.insert(0, mText.toString().substring(unionStart, spanStart));
- suggestionInfo.text.append(mText.toString().substring(spanEnd, unionEnd));
+ final String textAsString = text.toString();
+ suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
+ suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
}
@Override
@@ -10128,8 +10164,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
boolean willExtract = extractedTextModeWillBeStarted();
- // Do not start the action mode when extracted text will show up full screen, thus
- // immediately hiding the newly created action bar, which would be visually distracting.
+ // Do not start the action mode when extracted text will show up full screen, which would
+ // immediately hide the newly created action bar and would be visually distracting.
if (!willExtract) {
ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
mSelectionActionMode = startActionMode(actionModeCallback);
@@ -10155,7 +10191,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
- private void stopSelectionActionMode() {
+ /**
+ * @hide
+ */
+ protected void stopSelectionActionMode() {
if (mSelectionActionMode != null) {
// This will hide the mSelectionModifierCursorController
mSelectionActionMode.finish();
@@ -10769,7 +10808,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
final float deltaX = mDownPositionX - ev.getRawX();
final float deltaY = mDownPositionY - ev.getRawY();
final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
- if (distanceSquared < mSquaredTouchSlopDistance) {
+
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ TextView.this.getContext());
+ final int touchSlop = viewConfiguration.getScaledTouchSlop();
+
+ if (distanceSquared < touchSlop * touchSlop) {
if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
// Tapping on the handle dismisses the displayed action popup
mActionPopupWindow.hide();
@@ -10983,7 +11027,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// Double tap detection
private long mPreviousTapUpTime = 0;
- private float mPreviousTapPositionX, mPreviousTapPositionY;
+ private float mDownPositionX, mDownPositionY;
+ private boolean mGestureStayedInTapRegion;
SelectionModifierCursorController() {
resetTouchOffsets();
@@ -11046,20 +11091,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
// Double tap detection
- long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
- if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
- isPositionOnText(x, y)) {
- final float deltaX = x - mPreviousTapPositionX;
- final float deltaY = y - mPreviousTapPositionY;
- final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
- if (distanceSquared < mSquaredTouchSlopDistance) {
- startSelectionActionMode();
- mDiscardNextActionUp = true;
+ if (mGestureStayedInTapRegion) {
+ long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
+ if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
+ final float deltaX = x - mDownPositionX;
+ final float deltaY = y - mDownPositionY;
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ TextView.this.getContext());
+ int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
+ boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
+
+ if (stayedInArea && isPositionOnText(x, y)) {
+ startSelectionActionMode();
+ mDiscardNextActionUp = true;
+ }
}
}
- mPreviousTapPositionX = x;
- mPreviousTapPositionY = y;
+ mDownPositionX = x;
+ mDownPositionY = y;
+ mGestureStayedInTapRegion = true;
break;
case MotionEvent.ACTION_POINTER_DOWN:
@@ -11072,6 +11125,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
break;
+ case MotionEvent.ACTION_MOVE:
+ if (mGestureStayedInTapRegion) {
+ final float deltaX = event.getX() - mDownPositionX;
+ final float deltaY = event.getY() - mDownPositionY;
+ final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+ TextView.this.getContext());
+ int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
+
+ if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
+ mGestureStayedInTapRegion = false;
+ }
+ }
+ break;
+
case MotionEvent.ACTION_UP:
mPreviousTapUpTime = SystemClock.uptimeMillis();
break;
@@ -11375,6 +11444,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
case TEXT_DIRECTION_RTL:
mTextDir = TextDirectionHeuristics.RTL;
break;
+ case TEXT_DIRECTION_LOCALE:
+ mTextDir = TextDirectionHeuristics.LOCALE;
+ break;
}
}
@@ -11504,13 +11576,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private boolean mUserSetTextScaleX;
private final Paint mHighlightPaint;
private int mHighlightColor = 0x6633B5E5;
- /**
- * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
- * this field being protected. Will be restored as private when lineHeight
- * feature request 3215097 is implemented
- * @hide
- */
- protected Layout mLayout;
+ private Layout mLayout;
private long mShowCursor;
private Blink mBlink;
diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java
index afca2db..8f10fff 100644
--- a/core/java/android/widget/TimePicker.java
+++ b/core/java/android/widget/TimePicker.java
@@ -27,6 +27,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.NumberPicker.OnValueChangeListener;
@@ -476,6 +477,18 @@ public class TimePicker extends FrameLayout {
event.getText().add(selectedDateUtterance);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TimePicker.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TimePicker.class.getName());
+ }
+
private void updateHourControl() {
if (is24HourView()) {
mHourSpinner.setMinValue(0);
diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java
index a754268..a0edafe 100644
--- a/core/java/android/widget/ToggleButton.java
+++ b/core/java/android/widget/ToggleButton.java
@@ -23,6 +23,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.R;
@@ -161,4 +162,16 @@ public class ToggleButton extends CompoundButton {
event.getText().add(mContext.getString(R.string.togglebutton_not_pressed));
}
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ToggleButton.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ToggleButton.class.getName());
+ }
}
diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java
index eab6f2d..e707ea3 100644
--- a/core/java/android/widget/TwoLineListItem.java
+++ b/core/java/android/widget/TwoLineListItem.java
@@ -16,14 +16,12 @@
package android.widget;
-import com.android.internal.R;
-
-
import android.annotation.Widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
-import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RelativeLayout;
/**
@@ -86,4 +84,16 @@ public class TwoLineListItem extends RelativeLayout {
public TextView getText2() {
return mText2;
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(TwoLineListItem.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(TwoLineListItem.class.getName());
+ }
}
diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java
index 8e438ff..0fba498 100644
--- a/core/java/android/widget/VideoView.java
+++ b/core/java/android/widget/VideoView.java
@@ -34,6 +34,8 @@ import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.MediaController.MediaPlayerControl;
import java.io.IOException;
@@ -124,6 +126,18 @@ public class VideoView extends SurfaceView implements MediaPlayerControl {
setMeasuredDimension(width, height);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(VideoView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(VideoView.class.getName());
+ }
+
public int resolveAdjustedSize(int desiredSize, int measureSpec) {
int result = desiredSize;
int specMode = MeasureSpec.getMode(measureSpec);
@@ -380,7 +394,6 @@ public class VideoView extends SurfaceView implements MediaPlayerControl {
}
new AlertDialog.Builder(mContext)
- .setTitle(com.android.internal.R.string.VideoView_error_title)
.setMessage(messageId)
.setPositiveButton(com.android.internal.R.string.VideoView_error_button,
new DialogInterface.OnClickListener() {
diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java
index 3c683d6..6a68240 100644
--- a/core/java/android/widget/ViewAnimator.java
+++ b/core/java/android/widget/ViewAnimator.java
@@ -22,6 +22,8 @@ import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
@@ -185,6 +187,10 @@ public class ViewAnimator extends FrameLayout {
} else {
child.setVisibility(View.GONE);
}
+ if (index >= 0 && mWhichChild >= index) {
+ // Added item above current one, increment the index of the displayed child
+ setDisplayedChild(mWhichChild + 1);
+ }
}
@Override
@@ -337,4 +343,16 @@ public class ViewAnimator extends FrameLayout {
public int getBaseline() {
return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline();
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ViewAnimator.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ViewAnimator.class.getName());
+ }
}
diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java
index c6f6e81..061bb00 100644
--- a/core/java/android/widget/ViewFlipper.java
+++ b/core/java/android/widget/ViewFlipper.java
@@ -25,6 +25,8 @@ import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews.RemoteView;
/**
@@ -139,6 +141,18 @@ public class ViewFlipper extends ViewAnimator {
updateRunning();
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ViewFlipper.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ViewFlipper.class.getName());
+ }
+
/**
* Internal method to start or stop dispatching flip {@link Message} based
* on {@link #mRunning} and {@link #mVisible} state.
diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java
index 71ae624..0376918 100644
--- a/core/java/android/widget/ViewSwitcher.java
+++ b/core/java/android/widget/ViewSwitcher.java
@@ -20,6 +20,8 @@ import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
/**
* {@link ViewAnimator} that switches between two views, and has a factory
@@ -66,6 +68,18 @@ public class ViewSwitcher extends ViewAnimator {
super.addView(child, index, params);
}
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ViewSwitcher.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ViewSwitcher.class.getName());
+ }
+
/**
* Returns the next view to be displayed.
*
diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java
index eb372ca..af17c94 100644
--- a/core/java/android/widget/ZoomButton.java
+++ b/core/java/android/widget/ZoomButton.java
@@ -23,6 +23,8 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLongClickListener;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
public class ZoomButton extends ImageButton implements OnLongClickListener {
@@ -96,4 +98,16 @@ public class ZoomButton extends ImageButton implements OnLongClickListener {
clearFocus();
return super.dispatchUnhandledMove(focused, direction);
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ZoomButton.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ZoomButton.class.getName());
+ }
}
diff --git a/core/java/android/widget/ZoomControls.java b/core/java/android/widget/ZoomControls.java
index a12aee5..8897875 100644
--- a/core/java/android/widget/ZoomControls.java
+++ b/core/java/android/widget/ZoomControls.java
@@ -22,6 +22,8 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import com.android.internal.R;
@@ -106,4 +108,16 @@ public class ZoomControls extends LinearLayout {
public boolean hasFocus() {
return mZoomIn.hasFocus() || mZoomOut.hasFocus();
}
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(ZoomControls.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(ZoomControls.class.getName());
+ }
}
diff --git a/core/java/com/android/internal/backup/BackupConstants.java b/core/java/com/android/internal/backup/BackupConstants.java
index 906b5d5..4c276b7 100644
--- a/core/java/com/android/internal/backup/BackupConstants.java
+++ b/core/java/com/android/internal/backup/BackupConstants.java
@@ -24,4 +24,5 @@ public class BackupConstants {
public static final int TRANSPORT_ERROR = 1;
public static final int TRANSPORT_NOT_INITIALIZED = 2;
public static final int AGENT_ERROR = 3;
+ public static final int AGENT_UNKNOWN = 4;
}
diff --git a/core/java/com/android/internal/net/DNParser.java b/core/java/com/android/internal/net/DNParser.java
deleted file mode 100644
index 5254207..0000000
--- a/core/java/com/android/internal/net/DNParser.java
+++ /dev/null
@@ -1,450 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.net;
-
-
-import android.util.Log;
-
-import java.io.IOException;
-
-import javax.security.auth.x500.X500Principal;
-
-/**
- * A simple distinguished name(DN) parser.
- *
- * <p>This class is based on org.apache.harmony.security.x509.DNParser. It's customized to remove
- * external references which are unnecessary for our requirements.
- *
- * <p>This class is only meant for extracting a string value from a DN. e.g. it doesn't support
- * values in the hex-string style.
- *
- * <p>This class is used by {@link DomainNameValidator} only. However, in order to make this
- * class visible from unit tests, it's made public.
- *
- * @hide
- */
-public final class DNParser {
- private static final String TAG = "DNParser";
-
- /** DN to be parsed. */
- private final String dn;
-
- // length of distinguished name string
- private final int length;
-
- private int pos, beg, end;
-
- // tmp vars to store positions of the currently parsed item
- private int cur;
-
- // distinguished name chars
- private char[] chars;
-
- /**
- * Exception message thrown when we failed to parse DN, which shouldn't happen because we
- * only handle DNs that {@link X500Principal#getName} returns, which shouldn't be malformed.
- */
- private static final String ERROR_PARSE_ERROR = "Failed to parse DN";
-
- /**
- * Constructor.
- *
- * @param principal - {@link X500Principal} to be parsed
- */
- public DNParser(X500Principal principal) {
- this.dn = principal.getName(X500Principal.RFC2253);
- this.length = dn.length();
- }
-
- // gets next attribute type: (ALPHA 1*keychar) / oid
- private String nextAT() throws IOException {
-
- // skip preceding space chars, they can present after
- // comma or semicolon (compatibility with RFC 1779)
- for (; pos < length && chars[pos] == ' '; pos++) {
- }
- if (pos == length) {
- return null; // reached the end of DN
- }
-
- // mark the beginning of attribute type
- beg = pos;
-
- // attribute type chars
- pos++;
- for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
- // we don't follow exact BNF syntax here:
- // accept any char except space and '='
- }
- if (pos >= length) {
- // unexpected end of DN
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- // mark the end of attribute type
- end = pos;
-
- // skip trailing space chars between attribute type and '='
- // (compatibility with RFC 1779)
- if (chars[pos] == ' ') {
- for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
- }
-
- if (chars[pos] != '=' || pos == length) {
- // unexpected end of DN
- throw new IOException(ERROR_PARSE_ERROR);
- }
- }
-
- pos++; //skip '=' char
-
- // skip space chars between '=' and attribute value
- // (compatibility with RFC 1779)
- for (; pos < length && chars[pos] == ' '; pos++) {
- }
-
- // in case of oid attribute type skip its prefix: "oid." or "OID."
- // (compatibility with RFC 1779)
- if ((end - beg > 4) && (chars[beg + 3] == '.')
- && (chars[beg] == 'O' || chars[beg] == 'o')
- && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
- && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
- beg += 4;
- }
-
- return new String(chars, beg, end - beg);
- }
-
- // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
- private String quotedAV() throws IOException {
-
- pos++;
- beg = pos;
- end = beg;
- while (true) {
-
- if (pos == length) {
- // unexpected end of DN
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- if (chars[pos] == '"') {
- // enclosing quotation was found
- pos++;
- break;
- } else if (chars[pos] == '\\') {
- chars[end] = getEscaped();
- } else {
- // shift char: required for string with escaped chars
- chars[end] = chars[pos];
- }
- pos++;
- end++;
- }
-
- // skip trailing space chars before comma or semicolon.
- // (compatibility with RFC 1779)
- for (; pos < length && chars[pos] == ' '; pos++) {
- }
-
- return new String(chars, beg, end - beg);
- }
-
- // gets hex string attribute value: "#" hexstring
- private String hexAV() throws IOException {
-
- if (pos + 4 >= length) {
- // encoded byte array must be not less then 4 c
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- beg = pos; // store '#' position
- pos++;
- while (true) {
-
- // check for end of attribute value
- // looks for space and component separators
- if (pos == length || chars[pos] == '+' || chars[pos] == ','
- || chars[pos] == ';') {
- end = pos;
- break;
- }
-
- if (chars[pos] == ' ') {
- end = pos;
- pos++;
- // skip trailing space chars before comma or semicolon.
- // (compatibility with RFC 1779)
- for (; pos < length && chars[pos] == ' '; pos++) {
- }
- break;
- } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
- chars[pos] += 32; //to low case
- }
-
- pos++;
- }
-
- // verify length of hex string
- // encoded byte array must be not less then 4 and must be even number
- int hexLen = end - beg; // skip first '#' char
- if (hexLen < 5 || (hexLen & 1) == 0) {
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- // get byte encoding from string representation
- byte[] encoded = new byte[hexLen / 2];
- for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
- encoded[i] = (byte) getByte(p);
- }
-
- return new String(chars, beg, hexLen);
- }
-
- // gets string attribute value: *( stringchar / pair )
- private String escapedAV() throws IOException {
-
- beg = pos;
- end = pos;
- while (true) {
-
- if (pos >= length) {
- // the end of DN has been found
- return new String(chars, beg, end - beg);
- }
-
- switch (chars[pos]) {
- case '+':
- case ',':
- case ';':
- // separator char has beed found
- return new String(chars, beg, end - beg);
- case '\\':
- // escaped char
- chars[end++] = getEscaped();
- pos++;
- break;
- case ' ':
- // need to figure out whether space defines
- // the end of attribute value or not
- cur = end;
-
- pos++;
- chars[end++] = ' ';
-
- for (; pos < length && chars[pos] == ' '; pos++) {
- chars[end++] = ' ';
- }
- if (pos == length || chars[pos] == ',' || chars[pos] == '+'
- || chars[pos] == ';') {
- // separator char or the end of DN has beed found
- return new String(chars, beg, cur - beg);
- }
- break;
- default:
- chars[end++] = chars[pos];
- pos++;
- }
- }
- }
-
- // returns escaped char
- private char getEscaped() throws IOException {
-
- pos++;
- if (pos == length) {
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- switch (chars[pos]) {
- case '"':
- case '\\':
- case ',':
- case '=':
- case '+':
- case '<':
- case '>':
- case '#':
- case ';':
- case ' ':
- case '*':
- case '%':
- case '_':
- //FIXME: escaping is allowed only for leading or trailing space char
- return chars[pos];
- default:
- // RFC doesn't explicitly say that escaped hex pair is
- // interpreted as UTF-8 char. It only contains an example of such DN.
- return getUTF8();
- }
- }
-
- // decodes UTF-8 char
- // see http://www.unicode.org for UTF-8 bit distribution table
- private char getUTF8() throws IOException {
-
- int res = getByte(pos);
- pos++; //FIXME tmp
-
- if (res < 128) { // one byte: 0-7F
- return (char) res;
- } else if (res >= 192 && res <= 247) {
-
- int count;
- if (res <= 223) { // two bytes: C0-DF
- count = 1;
- res = res & 0x1F;
- } else if (res <= 239) { // three bytes: E0-EF
- count = 2;
- res = res & 0x0F;
- } else { // four bytes: F0-F7
- count = 3;
- res = res & 0x07;
- }
-
- int b;
- for (int i = 0; i < count; i++) {
- pos++;
- if (pos == length || chars[pos] != '\\') {
- return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
- }
- pos++;
-
- b = getByte(pos);
- pos++; //FIXME tmp
- if ((b & 0xC0) != 0x80) {
- return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
- }
-
- res = (res << 6) + (b & 0x3F);
- }
- return (char) res;
- } else {
- return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
- }
- }
-
- // Returns byte representation of a char pair
- // The char pair is composed of DN char in
- // specified 'position' and the next char
- // According to BNF syntax:
- // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
- // / "a" / "b" / "c" / "d" / "e" / "f"
- private int getByte(int position) throws IOException {
-
- if ((position + 1) >= length) {
- // to avoid ArrayIndexOutOfBoundsException
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- int b1, b2;
-
- b1 = chars[position];
- if (b1 >= '0' && b1 <= '9') {
- b1 = b1 - '0';
- } else if (b1 >= 'a' && b1 <= 'f') {
- b1 = b1 - 87; // 87 = 'a' - 10
- } else if (b1 >= 'A' && b1 <= 'F') {
- b1 = b1 - 55; // 55 = 'A' - 10
- } else {
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- b2 = chars[position + 1];
- if (b2 >= '0' && b2 <= '9') {
- b2 = b2 - '0';
- } else if (b2 >= 'a' && b2 <= 'f') {
- b2 = b2 - 87; // 87 = 'a' - 10
- } else if (b2 >= 'A' && b2 <= 'F') {
- b2 = b2 - 55; // 55 = 'A' - 10
- } else {
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- return (b1 << 4) + b2;
- }
-
- /**
- * Parses the DN and returns the attribute value for an attribute type.
- *
- * @param attributeType attribute type to look for (e.g. "ca")
- * @return value of the attribute that first found, or null if none found
- */
- public String find(String attributeType) {
- try {
- // Initialize internal state.
- pos = 0;
- beg = 0;
- end = 0;
- cur = 0;
- chars = dn.toCharArray();
-
- String attType = nextAT();
- if (attType == null) {
- return null;
- }
- while (true) {
- String attValue = "";
-
- if (pos == length) {
- return null;
- }
-
- switch (chars[pos]) {
- case '"':
- attValue = quotedAV();
- break;
- case '#':
- attValue = hexAV();
- break;
- case '+':
- case ',':
- case ';': // compatibility with RFC 1779: semicolon can separate RDNs
- //empty attribute value
- break;
- default:
- attValue = escapedAV();
- }
-
- if (attributeType.equalsIgnoreCase(attType)) {
- return attValue;
- }
-
- if (pos >= length) {
- return null;
- }
-
- if (chars[pos] == ',' || chars[pos] == ';') {
- } else if (chars[pos] != '+') {
- throw new IOException(ERROR_PARSE_ERROR);
- }
-
- pos++;
- attType = nextAT();
- if (attType == null) {
- throw new IOException(ERROR_PARSE_ERROR);
- }
- }
- } catch (IOException e) {
- // Parse error shouldn't happen, because we only handle DNs that
- // X500Principal.getName() returns, which shouldn't be malformed.
- Log.e(TAG, "Failed to parse DN: " + dn);
- return null;
- }
- }
-}
diff --git a/core/java/com/android/internal/net/DomainNameValidator.java b/core/java/com/android/internal/net/DomainNameValidator.java
deleted file mode 100644
index 3950655..0000000
--- a/core/java/com/android/internal/net/DomainNameValidator.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.internal.net;
-
-import android.net.NetworkUtils;
-import android.util.Log;
-
-import java.net.InetAddress;
-import java.security.cert.CertificateParsingException;
-import java.security.cert.X509Certificate;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-
-import javax.security.auth.x500.X500Principal;
-
-/** @hide */
-public class DomainNameValidator {
- private final static String TAG = "DomainNameValidator";
-
- private static final boolean DEBUG = false;
- private static final boolean LOG_ENABLED = false;
-
- private static final int ALT_DNS_NAME = 2;
- private static final int ALT_IPA_NAME = 7;
-
- /**
- * Checks the site certificate against the domain name of the site being visited
- * @param certificate The certificate to check
- * @param thisDomain The domain name of the site being visited
- * @return True iff if there is a domain match as specified by RFC2818
- */
- public static boolean match(X509Certificate certificate, String thisDomain) {
- if (certificate == null || thisDomain == null || thisDomain.length() == 0) {
- return false;
- }
-
- thisDomain = thisDomain.toLowerCase();
- if (!isIpAddress(thisDomain)) {
- return matchDns(certificate, thisDomain);
- } else {
- return matchIpAddress(certificate, thisDomain);
- }
- }
-
- /**
- * @return True iff the domain name is specified as an IP address
- */
- private static boolean isIpAddress(String domain) {
- boolean rval = (domain != null && domain.length() != 0);
- if (rval) {
- try {
- // do a quick-dirty IP match first to avoid DNS lookup
- rval = domain.equals(
- NetworkUtils.numericToInetAddress(domain).getHostAddress());
- } catch (IllegalArgumentException e) {
- if (LOG_ENABLED) {
- Log.v(TAG, "DomainNameValidator.isIpAddress(): " + e);
- }
-
- rval = false;
- }
- }
-
- return rval;
- }
-
- /**
- * Checks the site certificate against the IP domain name of the site being visited
- * @param certificate The certificate to check
- * @param thisDomain The DNS domain name of the site being visited
- * @return True iff if there is a domain match as specified by RFC2818
- */
- private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) {
- if (LOG_ENABLED) {
- Log.v(TAG, "DomainNameValidator.matchIpAddress(): this domain: " + thisDomain);
- }
-
- try {
- Collection subjectAltNames = certificate.getSubjectAlternativeNames();
- if (subjectAltNames != null) {
- Iterator i = subjectAltNames.iterator();
- while (i.hasNext()) {
- List altNameEntry = (List)(i.next());
- if (altNameEntry != null && 2 <= altNameEntry.size()) {
- Integer altNameType = (Integer)(altNameEntry.get(0));
- if (altNameType != null) {
- if (altNameType.intValue() == ALT_IPA_NAME) {
- String altName = (String)(altNameEntry.get(1));
- if (altName != null) {
- if (LOG_ENABLED) {
- Log.v(TAG, "alternative IP: " + altName);
- }
- if (thisDomain.equalsIgnoreCase(altName)) {
- return true;
- }
- }
- }
- }
- }
- }
- }
- } catch (CertificateParsingException e) {}
-
- return false;
- }
-
- /**
- * Checks the site certificate against the DNS domain name of the site being visited
- * @param certificate The certificate to check
- * @param thisDomain The DNS domain name of the site being visited
- * @return True iff if there is a domain match as specified by RFC2818
- */
- private static boolean matchDns(X509Certificate certificate, String thisDomain) {
- boolean hasDns = false;
- try {
- Collection subjectAltNames = certificate.getSubjectAlternativeNames();
- if (subjectAltNames != null) {
- Iterator i = subjectAltNames.iterator();
- while (i.hasNext()) {
- List altNameEntry = (List)(i.next());
- if (altNameEntry != null && 2 <= altNameEntry.size()) {
- Integer altNameType = (Integer)(altNameEntry.get(0));
- if (altNameType != null) {
- if (altNameType.intValue() == ALT_DNS_NAME) {
- hasDns = true;
- String altName = (String)(altNameEntry.get(1));
- if (altName != null) {
- if (matchDns(thisDomain, altName)) {
- return true;
- }
- }
- }
- }
- }
- }
- }
- } catch (CertificateParsingException e) {
- String errorMessage = e.getMessage();
- if (errorMessage == null) {
- errorMessage = "failed to parse certificate";
- }
-
- Log.w(TAG, "DomainNameValidator.matchDns(): " + errorMessage);
- return false;
- }
-
- if (!hasDns) {
- final String cn = new DNParser(certificate.getSubjectX500Principal())
- .find("cn");
- if (LOG_ENABLED) {
- Log.v(TAG, "Validating subject: DN:"
- + certificate.getSubjectX500Principal().getName(X500Principal.CANONICAL)
- + " CN:" + cn);
- }
- if (cn != null) {
- return matchDns(thisDomain, cn);
- }
- }
-
- return false;
- }
-
- /**
- * @param thisDomain The domain name of the site being visited
- * @param thatDomain The domain name from the certificate
- * @return True iff thisDomain matches thatDomain as specified by RFC2818
- */
- // not private for testing
- public static boolean matchDns(String thisDomain, String thatDomain) {
- if (LOG_ENABLED) {
- Log.v(TAG, "DomainNameValidator.matchDns():" +
- " this domain: " + thisDomain +
- " that domain: " + thatDomain);
- }
-
- if (thisDomain == null || thisDomain.length() == 0 ||
- thatDomain == null || thatDomain.length() == 0) {
- return false;
- }
-
- thatDomain = thatDomain.toLowerCase();
-
- // (a) domain name strings are equal, ignoring case: X matches X
- boolean rval = thisDomain.equals(thatDomain);
- if (!rval) {
- String[] thisDomainTokens = thisDomain.split("\\.");
- String[] thatDomainTokens = thatDomain.split("\\.");
-
- int thisDomainTokensNum = thisDomainTokens.length;
- int thatDomainTokensNum = thatDomainTokens.length;
-
- // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X
- if (thisDomainTokensNum >= thatDomainTokensNum) {
- for (int i = thatDomainTokensNum - 1; i >= 0; --i) {
- rval = thisDomainTokens[i].equals(thatDomainTokens[i]);
- if (!rval) {
- // (c) OR we have a special *-match:
- // *.Y.X matches Z.Y.X but *.X doesn't match Z.Y.X
- rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum);
- if (rval) {
- rval = thatDomainTokens[0].equals("*");
- if (!rval) {
- // (d) OR we have a *-component match:
- // f*.com matches foo.com but not bar.com
- rval = domainTokenMatch(
- thisDomainTokens[0], thatDomainTokens[0]);
- }
- }
- break;
- }
- }
- } else {
- // (e) OR thatHost has a '*.'-prefix of thisHost:
- // *.Y.X matches Y.X
- rval = thatDomain.equals("*." + thisDomain);
- }
- }
-
- return rval;
- }
-
- /**
- * @param thisDomainToken The domain token from the current domain name
- * @param thatDomainToken The domain token from the certificate
- * @return True iff thisDomainToken matches thatDomainToken, using the
- * wildcard match as specified by RFC2818-3.1. For example, f*.com must
- * match foo.com but not bar.com
- */
- private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) {
- if (thisDomainToken != null && thatDomainToken != null) {
- int starIndex = thatDomainToken.indexOf('*');
- if (starIndex >= 0) {
- if (thatDomainToken.length() - 1 <= thisDomainToken.length()) {
- String prefix = thatDomainToken.substring(0, starIndex);
- String suffix = thatDomainToken.substring(starIndex + 1);
-
- return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix);
- }
- }
- }
-
- return false;
- }
-}
diff --git a/core/java/com/android/internal/os/AtomicFile.java b/core/java/com/android/internal/os/AtomicFile.java
index b093977..445d10a 100644
--- a/core/java/com/android/internal/os/AtomicFile.java
+++ b/core/java/com/android/internal/os/AtomicFile.java
@@ -28,6 +28,17 @@ import java.io.IOException;
/**
* Helper class for performing atomic operations on a file, by creating a
* backup file until a write has successfully completed.
+ * <p>
+ * Atomic file guarantees file integrity by ensuring that a file has
+ * been completely written and sync'd to disk before removing its backup.
+ * As long as the backup file exists, the original file is considered
+ * to be invalid (left over from a previous attempt to write the file).
+ * </p><p>
+ * Atomic file does not confer any file locking semantics.
+ * Do not use this class when the file may be accessed or modified concurrently
+ * by multiple threads or processes. The caller is responsible for ensuring
+ * appropriate mutual exclusion invariants whenever it accesses the file.
+ * </p>
*/
public class AtomicFile {
private final File mBaseName;
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index fec4cbc..86118b1 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -19,6 +19,7 @@ package com.android.internal.os;
import static android.net.NetworkStats.IFACE_ALL;
import static android.net.NetworkStats.UID_ALL;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -35,6 +36,7 @@ import android.os.ParcelFormatException;
import android.os.Parcelable;
import android.os.Process;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.WorkSource;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
@@ -5713,11 +5715,17 @@ public final class BatteryStatsImpl extends BatteryStats {
synchronized (this) {
if (mNetworkSummaryCache == null
|| mNetworkSummaryCache.getElapsedRealtimeAge() > SECOND_IN_MILLIS) {
- try {
- mNetworkSummaryCache = mNetworkStatsFactory.readNetworkStatsSummary();
- } catch (IllegalStateException e) {
- // log problem and return empty object
- Log.wtf(TAG, "problem reading network stats", e);
+ mNetworkSummaryCache = null;
+
+ if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
+ try {
+ mNetworkSummaryCache = mNetworkStatsFactory.readNetworkStatsSummary();
+ } catch (IllegalStateException e) {
+ Log.wtf(TAG, "problem reading network stats", e);
+ }
+ }
+
+ if (mNetworkSummaryCache == null) {
mNetworkSummaryCache = new NetworkStats(SystemClock.elapsedRealtime(), 0);
}
}
@@ -5730,12 +5738,18 @@ public final class BatteryStatsImpl extends BatteryStats {
synchronized (this) {
if (mNetworkDetailCache == null
|| mNetworkDetailCache.getElapsedRealtimeAge() > SECOND_IN_MILLIS) {
- try {
- mNetworkDetailCache = mNetworkStatsFactory
- .readNetworkStatsDetail().groupedByUid();
- } catch (IllegalStateException e) {
- // log problem and return empty object
- Log.wtf(TAG, "problem reading network stats", e);
+ mNetworkDetailCache = null;
+
+ if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
+ try {
+ mNetworkDetailCache = mNetworkStatsFactory
+ .readNetworkStatsDetail().groupedByUid();
+ } catch (IllegalStateException e) {
+ Log.wtf(TAG, "problem reading network stats", e);
+ }
+ }
+
+ if (mNetworkDetailCache == null) {
mNetworkDetailCache = new NetworkStats(SystemClock.elapsedRealtime(), 0);
}
}
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 9c45dc6..6a99a2b 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -243,7 +243,7 @@ public class ZygoteInit {
private static void preloadClasses() {
final VMRuntime runtime = VMRuntime.getRuntime();
- InputStream is = ZygoteInit.class.getClassLoader().getResourceAsStream(
+ InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(
PRELOADED_CLASSES);
if (is == null) {
Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
diff --git a/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl b/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl
index 3c61968..ba0aa1a 100644
--- a/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl
+++ b/core/java/com/android/internal/textservice/ISpellCheckerSession.aidl
@@ -24,6 +24,7 @@ import android.view.textservice.TextInfo;
oneway interface ISpellCheckerSession {
void onGetSuggestionsMultiple(
in TextInfo[] textInfos, int suggestionsLimit, boolean multipleWords);
+ void onGetSuggestionsMultipleForSentence(in TextInfo[] textInfos, int suggestionsLimit);
void onCancel();
void onClose();
}
diff --git a/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl b/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl
index 796b06e..b44dbc8 100644
--- a/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl
+++ b/core/java/com/android/internal/textservice/ISpellCheckerSessionListener.aidl
@@ -23,4 +23,5 @@ import android.view.textservice.SuggestionsInfo;
*/
oneway interface ISpellCheckerSessionListener {
void onGetSuggestions(in SuggestionsInfo[] results);
+ void onGetSuggestionsForSentence(in SuggestionsInfo[] results);
}
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index 3d22929..d1aa1ce 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -17,7 +17,6 @@
package com.android.internal.util;
import java.lang.reflect.Array;
-import java.util.Collection;
// XXX these should be changed to reflect the actual memory allocator we use.
// it looks like right now objects want to be powers of 2 minus 8
@@ -142,4 +141,64 @@ public class ArrayUtils
}
return false;
}
+
+ public static long total(long[] array) {
+ long total = 0;
+ for (long value : array) {
+ total += value;
+ }
+ return total;
+ }
+
+ /**
+ * Appends an element to a copy of the array and returns the copy.
+ * @param array The original array, or null to represent an empty array.
+ * @param element The element to add.
+ * @return A new array that contains all of the elements of the original array
+ * with the specified element added at the end.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T[] appendElement(Class<T> kind, T[] array, T element) {
+ final T[] result;
+ final int end;
+ if (array != null) {
+ end = array.length;
+ result = (T[])Array.newInstance(kind, end + 1);
+ System.arraycopy(array, 0, result, 0, end);
+ } else {
+ end = 0;
+ result = (T[])Array.newInstance(kind, 1);
+ }
+ result[end] = element;
+ return result;
+ }
+
+ /**
+ * Removes an element from a copy of the array and returns the copy.
+ * If the element is not present, then the original array is returned unmodified.
+ * @param array The original array, or null to represent an empty array.
+ * @param element The element to remove.
+ * @return A new array that contains all of the elements of the original array
+ * except the first copy of the specified element removed. If the specified element
+ * was not present, then returns the original array. Returns null if the result
+ * would be an empty array.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T[] removeElement(Class<T> kind, T[] array, T element) {
+ if (array != null) {
+ final int length = array.length;
+ for (int i = 0; i < length; i++) {
+ if (array[i] == element) {
+ if (length == 1) {
+ return null;
+ }
+ T[] result = (T[])Array.newInstance(kind, length - 1);
+ System.arraycopy(array, 0, result, 0, i);
+ System.arraycopy(array, i + 1, result, i, length - i - 1);
+ return result;
+ }
+ }
+ }
+ return array;
+ }
}
diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java
new file mode 100644
index 0000000..8a8f315
--- /dev/null
+++ b/core/java/com/android/internal/util/FileRotator.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import android.os.FileUtils;
+import android.util.Slog;
+
+import com.android.internal.util.FileRotator.Rewriter;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import libcore.io.IoUtils;
+
+/**
+ * Utility that rotates files over time, similar to {@code logrotate}. There is
+ * a single "active" file, which is periodically rotated into historical files,
+ * and eventually deleted entirely. Files are stored under a specific directory
+ * with a well-known prefix.
+ * <p>
+ * Instead of manipulating files directly, users implement interfaces that
+ * perform operations on {@link InputStream} and {@link OutputStream}. This
+ * enables atomic rewriting of file contents in
+ * {@link #rewriteActive(Rewriter, long)}.
+ * <p>
+ * Users must periodically call {@link #maybeRotate(long)} to perform actual
+ * rotation. Not inherently thread safe.
+ */
+public class FileRotator {
+ private static final String TAG = "FileRotator";
+ private static final boolean LOGD = true;
+
+ private final File mBasePath;
+ private final String mPrefix;
+ private final long mRotateAgeMillis;
+ private final long mDeleteAgeMillis;
+
+ private static final String SUFFIX_BACKUP = ".backup";
+ private static final String SUFFIX_NO_BACKUP = ".no_backup";
+
+ // TODO: provide method to append to active file
+
+ /**
+ * External class that reads data from a given {@link InputStream}. May be
+ * called multiple times when reading rotated data.
+ */
+ public interface Reader {
+ public void read(InputStream in) throws IOException;
+ }
+
+ /**
+ * External class that writes data to a given {@link OutputStream}.
+ */
+ public interface Writer {
+ public void write(OutputStream out) throws IOException;
+ }
+
+ /**
+ * External class that reads existing data from given {@link InputStream},
+ * then writes any modified data to {@link OutputStream}.
+ */
+ public interface Rewriter extends Reader, Writer {
+ public void reset();
+ public boolean shouldWrite();
+ }
+
+ /**
+ * Create a file rotator.
+ *
+ * @param basePath Directory under which all files will be placed.
+ * @param prefix Filename prefix used to identify this rotator.
+ * @param rotateAgeMillis Age in milliseconds beyond which an active file
+ * may be rotated into a historical file.
+ * @param deleteAgeMillis Age in milliseconds beyond which a rotated file
+ * may be deleted.
+ */
+ public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) {
+ mBasePath = Preconditions.checkNotNull(basePath);
+ mPrefix = Preconditions.checkNotNull(prefix);
+ mRotateAgeMillis = rotateAgeMillis;
+ mDeleteAgeMillis = deleteAgeMillis;
+
+ // ensure that base path exists
+ mBasePath.mkdirs();
+
+ // recover any backup files
+ for (String name : mBasePath.list()) {
+ if (!name.startsWith(mPrefix)) continue;
+
+ if (name.endsWith(SUFFIX_BACKUP)) {
+ if (LOGD) Slog.d(TAG, "recovering " + name);
+
+ final File backupFile = new File(mBasePath, name);
+ final File file = new File(
+ mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
+
+ // write failed with backup; recover last file
+ backupFile.renameTo(file);
+
+ } else if (name.endsWith(SUFFIX_NO_BACKUP)) {
+ if (LOGD) Slog.d(TAG, "recovering " + name);
+
+ final File noBackupFile = new File(mBasePath, name);
+ final File file = new File(
+ mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
+
+ // write failed without backup; delete both
+ noBackupFile.delete();
+ file.delete();
+ }
+ }
+ }
+
+ /**
+ * Delete all files managed by this rotator.
+ */
+ public void deleteAll() {
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ // delete each file that matches parser
+ new File(mBasePath, name).delete();
+ }
+ }
+
+ /**
+ * Process currently active file, first reading any existing data, then
+ * writing modified data. Maintains a backup during write, which is restored
+ * if the write fails.
+ */
+ public void rewriteActive(Rewriter rewriter, long currentTimeMillis)
+ throws IOException {
+ final String activeName = getActiveName(currentTimeMillis);
+ rewriteSingle(rewriter, activeName);
+ }
+
+ @Deprecated
+ public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis)
+ throws IOException {
+ rewriteActive(new Rewriter() {
+ /** {@inheritDoc} */
+ public void reset() {
+ // ignored
+ }
+
+ /** {@inheritDoc} */
+ public void read(InputStream in) throws IOException {
+ reader.read(in);
+ }
+
+ /** {@inheritDoc} */
+ public boolean shouldWrite() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ public void write(OutputStream out) throws IOException {
+ writer.write(out);
+ }
+ }, currentTimeMillis);
+ }
+
+ /**
+ * Process all files managed by this rotator, usually to rewrite historical
+ * data. Each file is processed atomically.
+ */
+ public void rewriteAll(Rewriter rewriter) throws IOException {
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ // process each file that matches parser
+ rewriteSingle(rewriter, name);
+ }
+ }
+
+ /**
+ * Process a single file atomically, first reading any existing data, then
+ * writing modified data. Maintains a backup during write, which is restored
+ * if the write fails.
+ */
+ private void rewriteSingle(Rewriter rewriter, String name) throws IOException {
+ if (LOGD) Slog.d(TAG, "rewriting " + name);
+
+ final File file = new File(mBasePath, name);
+ final File backupFile;
+
+ rewriter.reset();
+
+ if (file.exists()) {
+ // read existing data
+ readFile(file, rewriter);
+
+ // skip when rewriter has nothing to write
+ if (!rewriter.shouldWrite()) return;
+
+ // backup existing data during write
+ backupFile = new File(mBasePath, name + SUFFIX_BACKUP);
+ file.renameTo(backupFile);
+
+ try {
+ writeFile(file, rewriter);
+
+ // write success, delete backup
+ backupFile.delete();
+ } catch (IOException e) {
+ // write failed, delete file and restore backup
+ file.delete();
+ backupFile.renameTo(file);
+ throw e;
+ }
+
+ } else {
+ // create empty backup during write
+ backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP);
+ backupFile.createNewFile();
+
+ try {
+ writeFile(file, rewriter);
+
+ // write success, delete empty backup
+ backupFile.delete();
+ } catch (IOException e) {
+ // write failed, delete file and empty backup
+ file.delete();
+ backupFile.delete();
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Read any rotated data that overlap the requested time range.
+ */
+ public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis)
+ throws IOException {
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ // read file when it overlaps
+ if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
+ if (LOGD) Slog.d(TAG, "reading matching " + name);
+
+ final File file = new File(mBasePath, name);
+ readFile(file, reader);
+ }
+ }
+ }
+
+ /**
+ * Return the currently active file, which may not exist yet.
+ */
+ private String getActiveName(long currentTimeMillis) {
+ String oldestActiveName = null;
+ long oldestActiveStart = Long.MAX_VALUE;
+
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ // pick the oldest active file which covers current time
+ if (info.isActive() && info.startMillis < currentTimeMillis
+ && info.startMillis < oldestActiveStart) {
+ oldestActiveName = name;
+ oldestActiveStart = info.startMillis;
+ }
+ }
+
+ if (oldestActiveName != null) {
+ return oldestActiveName;
+ } else {
+ // no active file found above; create one starting now
+ info.startMillis = currentTimeMillis;
+ info.endMillis = Long.MAX_VALUE;
+ return info.build();
+ }
+ }
+
+ /**
+ * Examine all files managed by this rotator, renaming or deleting if their
+ * age matches the configured thresholds.
+ */
+ public void maybeRotate(long currentTimeMillis) {
+ final long rotateBefore = currentTimeMillis - mRotateAgeMillis;
+ final long deleteBefore = currentTimeMillis - mDeleteAgeMillis;
+
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ if (info.isActive()) {
+ if (info.startMillis <= rotateBefore) {
+ // found active file; rotate if old enough
+ if (LOGD) Slog.d(TAG, "rotating " + name);
+
+ info.endMillis = currentTimeMillis;
+
+ final File file = new File(mBasePath, name);
+ final File destFile = new File(mBasePath, info.build());
+ file.renameTo(destFile);
+ }
+ } else if (info.endMillis <= deleteBefore) {
+ // found rotated file; delete if old enough
+ if (LOGD) Slog.d(TAG, "deleting " + name);
+
+ final File file = new File(mBasePath, name);
+ file.delete();
+ }
+ }
+ }
+
+ private static void readFile(File file, Reader reader) throws IOException {
+ final FileInputStream fis = new FileInputStream(file);
+ final BufferedInputStream bis = new BufferedInputStream(fis);
+ try {
+ reader.read(bis);
+ } finally {
+ IoUtils.closeQuietly(bis);
+ }
+ }
+
+ private static void writeFile(File file, Writer writer) throws IOException {
+ final FileOutputStream fos = new FileOutputStream(file);
+ final BufferedOutputStream bos = new BufferedOutputStream(fos);
+ try {
+ writer.write(bos);
+ bos.flush();
+ } finally {
+ FileUtils.sync(fos);
+ IoUtils.closeQuietly(bos);
+ }
+ }
+
+ /**
+ * Details for a rotated file, either parsed from an existing filename, or
+ * ready to be built into a new filename.
+ */
+ private static class FileInfo {
+ public final String prefix;
+
+ public long startMillis;
+ public long endMillis;
+
+ public FileInfo(String prefix) {
+ this.prefix = Preconditions.checkNotNull(prefix);
+ }
+
+ /**
+ * Attempt parsing the given filename.
+ *
+ * @return Whether parsing was successful.
+ */
+ public boolean parse(String name) {
+ startMillis = endMillis = -1;
+
+ final int dotIndex = name.lastIndexOf('.');
+ final int dashIndex = name.lastIndexOf('-');
+
+ // skip when missing time section
+ if (dotIndex == -1 || dashIndex == -1) return false;
+
+ // skip when prefix doesn't match
+ if (!prefix.equals(name.substring(0, dotIndex))) return false;
+
+ try {
+ startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex));
+
+ if (name.length() - dashIndex == 1) {
+ endMillis = Long.MAX_VALUE;
+ } else {
+ endMillis = Long.parseLong(name.substring(dashIndex + 1));
+ }
+
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Build current state into filename.
+ */
+ public String build() {
+ final StringBuilder name = new StringBuilder();
+ name.append(prefix).append('.').append(startMillis).append('-');
+ if (endMillis != Long.MAX_VALUE) {
+ name.append(endMillis);
+ }
+ return name.toString();
+ }
+
+ /**
+ * Test if current file is active (no end timestamp).
+ */
+ public boolean isActive() {
+ return endMillis == Long.MAX_VALUE;
+ }
+ }
+}
diff --git a/core/java/com/android/internal/util/IndentingPrintWriter.java b/core/java/com/android/internal/util/IndentingPrintWriter.java
new file mode 100644
index 0000000..3dd2284
--- /dev/null
+++ b/core/java/com/android/internal/util/IndentingPrintWriter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents
+ * newlines based on internal state. Delays writing indent until first actual
+ * write on a newline, enabling indent modification after newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+ private final String mIndent;
+
+ private StringBuilder mBuilder = new StringBuilder();
+ private String mCurrent = new String();
+ private boolean mEmptyLine = true;
+
+ public IndentingPrintWriter(Writer writer, String indent) {
+ super(writer);
+ mIndent = indent;
+ }
+
+ public void increaseIndent() {
+ mBuilder.append(mIndent);
+ mCurrent = mBuilder.toString();
+ }
+
+ public void decreaseIndent() {
+ mBuilder.delete(0, mIndent.length());
+ mCurrent = mBuilder.toString();
+ }
+
+ @Override
+ public void println() {
+ super.println();
+ mEmptyLine = true;
+ }
+
+ @Override
+ public void write(char[] buf, int offset, int count) {
+ if (mEmptyLine) {
+ mEmptyLine = false;
+ super.print(mCurrent);
+ }
+ super.write(buf, offset, count);
+ }
+}
diff --git a/core/java/com/android/internal/view/BaseInputHandler.java b/core/java/com/android/internal/view/BaseInputHandler.java
deleted file mode 100644
index 74b4b06..0000000
--- a/core/java/com/android/internal/view/BaseInputHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.view;
-
-import android.view.InputHandler;
-import android.view.InputQueue;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-/**
- * Base do-nothing implementation of an input handler.
- * @hide
- */
-public abstract class BaseInputHandler implements InputHandler {
- public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) {
- finishedCallback.finished(false);
- }
-
- public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) {
- finishedCallback.finished(false);
- }
-}
diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java
index a235d9a..9024d8d 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -387,9 +387,9 @@ public class InputConnectionWrapper implements InputConnection {
}
}
- public boolean deleteSurroundingText(int leftLength, int rightLength) {
+ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
try {
- mIInputContext.deleteSurroundingText(leftLength, rightLength);
+ mIInputContext.deleteSurroundingText(beforeLength, afterLength);
return true;
} catch (RemoteException e) {
return false;
diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java
index b689f53..517ce4e 100644
--- a/core/java/com/android/internal/widget/ActionBarView.java
+++ b/core/java/com/android/internal/widget/ActionBarView.java
@@ -300,6 +300,7 @@ public class ActionBarView extends AbsActionBarView {
mProgressView = new ProgressBar(mContext, null, 0, mProgressStyle);
mProgressView.setId(R.id.progress_horizontal);
mProgressView.setMax(10000);
+ mProgressView.setVisibility(GONE);
addView(mProgressView);
}
@@ -307,6 +308,7 @@ public class ActionBarView extends AbsActionBarView {
mIndeterminateProgressView = new ProgressBar(mContext, null, 0,
mIndeterminateProgressStyle);
mIndeterminateProgressView.setId(R.id.progress_circular);
+ mIndeterminateProgressView.setVisibility(GONE);
addView(mIndeterminateProgressView);
}
diff --git a/core/java/com/android/internal/widget/DigitalClock.java b/core/java/com/android/internal/widget/DigitalClock.java
index daefc9a..af3fd42 100644
--- a/core/java/com/android/internal/widget/DigitalClock.java
+++ b/core/java/com/android/internal/widget/DigitalClock.java
@@ -228,7 +228,7 @@ public class DigitalClock extends RelativeLayout {
updateTime();
}
- private void updateTime() {
+ public void updateTime() {
mCalendar.setTimeInMillis(System.currentTimeMillis());
CharSequence newTime = DateFormat.format(mFormat, mCalendar);
diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java
index 32e733b..9579bce 100644
--- a/core/java/com/android/internal/widget/EditableInputConnection.java
+++ b/core/java/com/android/internal/widget/EditableInputConnection.java
@@ -35,6 +35,11 @@ public class EditableInputConnection extends BaseInputConnection {
private final TextView mTextView;
+ // Keeps track of nested begin/end batch edit to ensure this connection always has a
+ // balanced impact on its associated TextView.
+ // A negative value means that this connection has been finished by the InputMethodManager.
+ private int mBatchEditNesting;
+
public EditableInputConnection(TextView textview) {
super(textview, true);
mTextView = textview;
@@ -48,19 +53,35 @@ public class EditableInputConnection extends BaseInputConnection {
}
return null;
}
-
+
@Override
public boolean beginBatchEdit() {
- mTextView.beginBatchEdit();
- return true;
+ synchronized(this) {
+ if (mBatchEditNesting >= 0) {
+ mTextView.beginBatchEdit();
+ mBatchEditNesting++;
+ return true;
+ }
+ }
+ return false;
}
-
+
@Override
public boolean endBatchEdit() {
- mTextView.endBatchEdit();
- return true;
+ synchronized(this) {
+ if (mBatchEditNesting > 0) {
+ // When the connection is reset by the InputMethodManager and finishComposingText
+ // is called, some endBatchEdit calls may still be asynchronously received from the
+ // IME. Do not take these into account, thus ensuring that this IC's final
+ // contribution to mTextView's nested batch edit count is zero.
+ mTextView.endBatchEdit();
+ mBatchEditNesting--;
+ return true;
+ }
+ }
+ return false;
}
-
+
@Override
public boolean clearMetaKeyStates(int states) {
final Editable content = getEditable();
@@ -76,7 +97,24 @@ public class EditableInputConnection extends BaseInputConnection {
}
return true;
}
-
+
+ @Override
+ public boolean finishComposingText() {
+ final boolean superResult = super.finishComposingText();
+ synchronized(this) {
+ if (mBatchEditNesting < 0) {
+ // The connection was already finished
+ return false;
+ }
+ while (mBatchEditNesting > 0) {
+ endBatchEdit();
+ }
+ // Will prevent any further calls to begin or endBatchEdit
+ mBatchEditNesting = -1;
+ }
+ return superResult;
+ }
+
@Override
public boolean commitCompletion(CompletionInfo text) {
if (DEBUG) Log.v(TAG, "commitCompletion " + text);
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 905a171..6893ffb 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -404,7 +404,7 @@ public class LockPatternUtils {
saveLockPassword(null, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
setLockPatternEnabled(false);
saveLockPattern(null);
- setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
+ setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED);
setLong(PASSWORD_TYPE_ALTERNATE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED);
}
diff --git a/core/java/com/android/server/NetworkManagementSocketTagger.java b/core/java/com/android/server/NetworkManagementSocketTagger.java
index 8445ad1..c77992d 100644
--- a/core/java/com/android/server/NetworkManagementSocketTagger.java
+++ b/core/java/com/android/server/NetworkManagementSocketTagger.java
@@ -80,14 +80,15 @@ public final class NetworkManagementSocketTagger extends SocketTagger {
}
private void tagSocketFd(FileDescriptor fd, int tag, int uid) {
- int errno;
if (tag == -1 && uid == -1) return;
- errno = native_tagSocketFd(fd, tag, uid);
- if (errno < 0) {
- Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", "
- + tag + ", " +
- + uid + ") failed with errno" + errno);
+ if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
+ final int errno = native_tagSocketFd(fd, tag, uid);
+ if (errno < 0) {
+ Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", "
+ + tag + ", " +
+ + uid + ") failed with errno" + errno);
+ }
}
}
@@ -101,12 +102,13 @@ public final class NetworkManagementSocketTagger extends SocketTagger {
private void unTagSocketFd(FileDescriptor fd) {
final SocketTags options = threadSocketTags.get();
- int errno;
if (options.statsTag == -1 && options.statsUid == -1) return;
- errno = native_untagSocketFd(fd);
- if (errno < 0) {
- Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno);
+ if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
+ final int errno = native_untagSocketFd(fd);
+ if (errno < 0) {
+ Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno);
+ }
}
}
@@ -116,16 +118,21 @@ public final class NetworkManagementSocketTagger extends SocketTagger {
}
public static void setKernelCounterSet(int uid, int counterSet) {
- int errno = native_setCounterSet(counterSet, uid);
- if (errno < 0) {
- Log.w(TAG, "setKernelCountSet(" + uid + ", " + counterSet + ") failed with errno " + errno);
+ if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
+ final int errno = native_setCounterSet(counterSet, uid);
+ if (errno < 0) {
+ Log.w(TAG, "setKernelCountSet(" + uid + ", " + counterSet + ") failed with errno "
+ + errno);
+ }
}
}
public static void resetKernelUidStats(int uid) {
- int errno = native_deleteTagData(0, uid);
- if (errno < 0) {
- Slog.w(TAG, "problem clearing counters for uid " + uid + " : errno " + errno);
+ if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
+ int errno = native_deleteTagData(0, uid);
+ if (errno < 0) {
+ Slog.w(TAG, "problem clearing counters for uid " + uid + " : errno " + errno);
+ }
}
}
diff --git a/core/java/com/google/android/mms/pdu/PduParser.java b/core/java/com/google/android/mms/pdu/PduParser.java
index f7f71ed..015d864 100755
--- a/core/java/com/google/android/mms/pdu/PduParser.java
+++ b/core/java/com/google/android/mms/pdu/PduParser.java
@@ -934,6 +934,9 @@ public class PduParser {
int temp = pduDataStream.read();
assert(-1 != temp);
int first = temp & 0xFF;
+ if (first == 0) {
+ return null; // Blank subject, bail.
+ }
pduDataStream.reset();
if (first < TEXT_MIN) {